@opengeni/db 1.0.1 → 3.2.0-canary.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/attached-browser-devices.d.ts +7 -0
- package/dist/automations.d.ts +154 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-integrations.d.ts +13 -0
- package/dist/child-lifecycle-notices.d.ts +68 -0
- package/dist/{chunk-4SR4HKKA.js → chunk-2EH4WRHL.js} +19 -11
- package/dist/chunk-2EH4WRHL.js.map +1 -0
- package/dist/{chunk-CUPM3FGB.js → chunk-2PRS4J5Q.js} +3 -3
- package/dist/chunk-6HMM2R5T.js +390 -0
- package/dist/chunk-6HMM2R5T.js.map +1 -0
- package/dist/{chunk-ZWREYEWA.js → chunk-AZHW7R5V.js} +56 -11
- package/dist/chunk-AZHW7R5V.js.map +1 -0
- package/dist/{chunk-LTG3S6ZY.js → chunk-E6CLMKEE.js} +2 -2
- package/dist/{chunk-6ZYKCRFO.js → chunk-KRLPEMLH.js} +7591 -4417
- package/dist/chunk-KRLPEMLH.js.map +1 -0
- package/dist/chunk-NAB6FKFN.js +516 -0
- package/dist/chunk-NAB6FKFN.js.map +1 -0
- package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
- package/dist/chunk-SIBYBA2D.js.map +1 -0
- package/dist/chunk-UZNXG44K.js +4508 -0
- package/dist/chunk-UZNXG44K.js.map +1 -0
- package/dist/codemode-operations.d.ts +1 -6
- package/dist/company-brain-context-selection-schema.d.ts +705 -0
- package/dist/company-brain-context-selection.d.ts +94 -0
- package/dist/company-brain-governed-writes.d.ts +24 -0
- package/dist/company-brain.d.ts +55 -0
- package/dist/company-profile-agent-admin.d.ts +15 -0
- package/dist/company-profile-schema.d.ts +619 -2
- package/dist/company-profile.d.ts +9 -4
- package/dist/connection-authority.d.ts +95 -0
- package/dist/connection-scopes.d.ts +8 -0
- package/dist/connection-tenancy-backfill.d.ts +85 -0
- package/dist/connection-token-resolver.d.ts +30 -1
- package/dist/database.d.ts +23 -4
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.d.ts +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/governed-learning-activation-schema.d.ts +984 -0
- package/dist/governed-learning-activation.d.ts +98 -0
- package/dist/governed-learning-evaluator-schema.d.ts +633 -0
- package/dist/governed-learning-evaluator.d.ts +28 -0
- package/dist/index.d.ts +1606 -81
- package/dist/index.js +21898 -8147
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +18 -0
- package/dist/integration-bindings.d.ts +1 -0
- package/dist/integration-facets.d.ts +30 -0
- package/dist/interaction-placement-loss.d.ts +22 -0
- package/dist/managed-human-provisioning.d.ts +25 -0
- package/dist/memory-domain.d.ts +3 -3
- package/dist/migrate.d.ts +11 -1
- package/dist/migrate.js +1 -1
- package/dist/new-session-drafts.d.ts +43 -5
- package/dist/organization-membership-backfill.d.ts +174 -0
- package/dist/organization-membership-lifecycle.d.ts +162 -0
- package/dist/pr-review.d.ts +107 -0
- package/dist/preference-registry-schema.d.ts +2 -0
- package/dist/preference-registry.d.ts +2 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +29 -8
- package/dist/schema.d.ts +24914 -13703
- package/dist/schema.js +99 -1
- package/dist/scoped-knowledge-schema.d.ts +52 -1
- package/dist/session-background-commands.d.ts +125 -0
- package/dist/session-background-commands.js +34 -0
- package/dist/session-background-commands.js.map +1 -0
- package/dist/session-control.d.ts +156 -3
- package/dist/session-goal-pacing.d.ts +68 -0
- package/dist/session-queue-commands.d.ts +43 -4
- package/dist/session-realtime-ledger.d.ts +2 -0
- package/dist/session-tenancy.d.ts +71 -1
- package/dist/session-tenancy.js +25 -3
- package/dist/session-tool-call-settlement.d.ts +9 -4
- package/dist/slack-routing-personal-workspace.d.ts +23 -0
- package/dist/slack-routing.d.ts +268 -0
- package/dist/slack-task-policy-schema.d.ts +34 -0
- package/dist/slack-task-policy.d.ts +8 -0
- package/dist/slack-user-link-access.d.ts +28 -0
- package/dist/task-notes-schema.d.ts +594 -0
- package/dist/task-notes.d.ts +39 -0
- package/dist/turn-initiator.d.ts +2 -0
- package/dist/user-resource-authority.d.ts +109 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-authority.d.ts +81 -0
- package/dist/workspace-instruction-policies-schema.d.ts +279 -0
- package/dist/workspace-instruction-policies.d.ts +62 -0
- package/dist/workspace-learning-policy.d.ts +1 -0
- package/dist/workspace-membership-permissions.d.ts +2 -0
- package/drizzle/0240_enrollment_connection_authority.sql +39 -0
- package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
- package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
- package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
- package/drizzle/0245_model_context_contribution_facts.sql +88 -0
- package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
- package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
- package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
- package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
- package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
- package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
- package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
- package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
- package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
- package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
- package/drizzle/0256_connection_authority_delegation.sql +703 -0
- package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
- package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
- package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
- package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
- package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
- package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
- package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
- package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
- package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
- package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
- package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
- package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
- package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
- package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
- package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
- package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
- package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
- package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
- package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
- package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
- package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
- package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
- package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
- package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
- package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
- package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
- package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
- package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
- package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
- package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
- package/drizzle/0289_session_composer_policy_authority.sql +597 -0
- package/drizzle/0290_organization_membership_backfill.sql +268 -0
- package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
- package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
- package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
- package/drizzle/0294_preference_activation_authority.sql +237 -0
- package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
- package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
- package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
- package/drizzle/0298_organization_tenancy_parity.sql +609 -0
- package/drizzle/0299_organization_membership_lock_order.sql +291 -0
- package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
- package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
- package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
- package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
- package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
- package/drizzle/0305_personal_resource_grant_management.sql +584 -0
- package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
- package/drizzle/0307_session_attention_state.sql +51 -0
- package/drizzle/0308_session_archives.sql +35 -0
- package/drizzle/0309_channel_project_pins.sql +6 -0
- package/drizzle/0310_channel_project_order.sql +27 -0
- package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
- package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
- package/drizzle/0313_private_child_session_authority.sql +365 -0
- package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
- package/drizzle/0315_personal_github_repository_selection.sql +624 -0
- package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
- package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
- package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
- package/drizzle/0319_event_triggered_automations.sql +254 -0
- package/drizzle/0320_pr_review_pack.sql +113 -0
- package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
- package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
- package/drizzle/0323_organization_private_session_enablement.sql +489 -0
- package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
- package/drizzle/0325_child_lifecycle_notices.sql +44 -0
- package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
- package/drizzle/0327_slack_first_task_hint.sql +29 -0
- package/drizzle/0328_session_background_commands.sql +590 -0
- package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
- package/drizzle/0330_api_key_descriptions.sql +20 -0
- package/drizzle/0331_managed_organization_creation.sql +163 -0
- package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
- package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
- package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
- package/drizzle/0335_slack_workspace_routing.sql +460 -0
- package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
- package/drizzle/0337_slack_routed_action_handles.sql +189 -0
- package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
- package/drizzle/0339_document_authority_reclassification.sql +1153 -0
- package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
- package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
- package/drizzle/0342_slack_route_prompt_single_pending.sql +32 -0
- package/drizzle/0343_personal_document_force_rls_lock_repair.sql +146 -0
- package/drizzle/0344_private_session_visibility_transition_gate.sql +85 -0
- package/drizzle/0345_tenant_scoped_session_tenancy_fence.sql +2839 -0
- package/drizzle/0346_document_migration_audit_surface.sql +454 -0
- package/drizzle/0347_connection_authority_convergence_evidence.sql +396 -0
- package/package.json +10 -6
- package/src/attached-browser-devices.ts +233 -5
- package/src/automations.ts +911 -0
- package/src/browser-sessions.ts +10 -0
- package/src/capability-integrations.ts +175 -1
- package/src/child-lifecycle-notices.ts +241 -0
- package/src/codemode-operations.ts +1 -35
- package/src/company-brain-context-selection-schema.ts +100 -0
- package/src/company-brain-context-selection.ts +359 -0
- package/src/company-brain-governed-writes.ts +1170 -0
- package/src/company-brain.ts +309 -0
- package/src/company-profile-agent-admin.ts +225 -0
- package/src/company-profile-schema.ts +121 -1
- package/src/company-profile.ts +21 -0
- package/src/computer-sessions.ts +10 -1
- package/src/connection-authority.ts +290 -0
- package/src/connection-scopes.ts +20 -0
- package/src/connection-tenancy-backfill.ts +167 -0
- package/src/connection-token-resolver.ts +263 -31
- package/src/database.ts +87 -2
- package/src/editable-artifacts-schema.ts +10 -5
- package/src/editable-artifacts.ts +21 -11
- package/src/governed-learning-activation-schema.ts +121 -0
- package/src/governed-learning-activation.ts +588 -0
- package/src/governed-learning-evaluator-schema.ts +72 -0
- package/src/governed-learning-evaluator.ts +217 -0
- package/src/index.ts +11676 -1550
- package/src/insights.ts +107 -0
- package/src/integration-bindings.ts +88 -6
- package/src/integration-facets.ts +287 -84
- package/src/interaction-placement-loss.ts +234 -0
- package/src/managed-human-provisioning.ts +116 -0
- package/src/memory-domain.ts +27 -32
- package/src/migrate.ts +77 -1
- package/src/new-session-drafts.ts +229 -35
- package/src/organization-membership-backfill.ts +735 -0
- package/src/organization-membership-lifecycle.ts +995 -0
- package/src/pr-review.ts +890 -0
- package/src/preference-registry.ts +31 -1
- package/src/provision-roles.ts +905 -6
- package/src/runtime-posture-cli.ts +2 -0
- package/src/runtime-posture.ts +1317 -72
- package/src/schema.ts +3316 -428
- package/src/scoped-knowledge-schema.ts +16 -8
- package/src/scoped-knowledge.ts +1 -1
- package/src/session-background-commands.ts +759 -0
- package/src/session-control.ts +640 -74
- package/src/session-goal-pacing.ts +126 -0
- package/src/session-queue-commands.ts +441 -85
- package/src/session-realtime-context.ts +4 -6
- package/src/session-realtime-ledger.ts +28 -14
- package/src/session-realtime-mirror.ts +1 -1
- package/src/session-tenancy.ts +410 -49
- package/src/session-tool-call-settlement.ts +38 -29
- package/src/slack-routing-personal-workspace.ts +67 -0
- package/src/slack-routing.ts +867 -0
- package/src/slack-task-policy-schema.ts +5 -0
- package/src/slack-task-policy.ts +18 -0
- package/src/slack-user-link-access.ts +138 -11
- package/src/task-notes-schema.ts +74 -0
- package/src/task-notes.ts +124 -2
- package/src/turn-initiator.ts +8 -0
- package/src/user-resource-authority.ts +377 -0
- package/src/workspace-authority.ts +160 -0
- package/src/workspace-instruction-policies-schema.ts +53 -1
- package/src/workspace-instruction-policies.ts +528 -77
- package/src/workspace-learning-policy.ts +21 -5
- package/src/workspace-membership-permissions.ts +9 -0
- package/dist/chunk-4SR4HKKA.js.map +0 -1
- package/dist/chunk-6ZYKCRFO.js.map +0 -1
- package/dist/chunk-NXAFLCQX.js +0 -2468
- package/dist/chunk-NXAFLCQX.js.map +0 -1
- package/dist/chunk-SK7YYHBI.js.map +0 -1
- package/dist/chunk-WD5ROSLL.js +0 -117
- package/dist/chunk-WD5ROSLL.js.map +0 -1
- package/dist/chunk-ZWREYEWA.js.map +0 -1
- /package/dist/{chunk-CUPM3FGB.js.map → chunk-2PRS4J5Q.js.map} +0 -0
- /package/dist/{chunk-LTG3S6ZY.js.map → chunk-E6CLMKEE.js.map} +0 -0
|
@@ -275,7 +275,7 @@ export async function flushSessionRealtimeTranscriptTailInTransaction(
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
const [session] = await db
|
|
278
|
-
.select({
|
|
278
|
+
.select({ reasoningEffort: schema.sessions.reasoningEffort })
|
|
279
279
|
.from(schema.sessions)
|
|
280
280
|
.where(
|
|
281
281
|
and(
|
|
@@ -285,7 +285,7 @@ export async function flushSessionRealtimeTranscriptTailInTransaction(
|
|
|
285
285
|
)
|
|
286
286
|
.limit(1);
|
|
287
287
|
if (!session) throw new Error(`Realtime tail session ${input.sessionId} disappeared`);
|
|
288
|
-
const
|
|
288
|
+
const reasoningEffort = ReasoningEffort.parse(session.reasoningEffort);
|
|
289
289
|
const admitted = await submitHumanPromptInTransaction(db, {
|
|
290
290
|
accountId: input.accountId,
|
|
291
291
|
workspaceId: input.workspaceId,
|
|
@@ -293,10 +293,8 @@ export async function flushSessionRealtimeTranscriptTailInTransaction(
|
|
|
293
293
|
subjectId: input.ownerSubjectId,
|
|
294
294
|
subjectLabel: "Realtime",
|
|
295
295
|
actor: {
|
|
296
|
-
type: "
|
|
296
|
+
type: "human",
|
|
297
297
|
subjectId: input.ownerSubjectId,
|
|
298
|
-
subjectLabel: "Realtime",
|
|
299
|
-
context: { source: SESSION_REALTIME_TAIL_SOURCE, realtimeId: mode.id },
|
|
300
298
|
},
|
|
301
299
|
operationKey: deterministicUuid(`opengeni:session-realtime-tail-flush:${mode.id}`),
|
|
302
300
|
delivery: "steer",
|
|
@@ -308,7 +306,7 @@ export async function flushSessionRealtimeTranscriptTailInTransaction(
|
|
|
308
306
|
context: rendered.context,
|
|
309
307
|
},
|
|
310
308
|
resources: [],
|
|
311
|
-
reasoningEffortFallback:
|
|
309
|
+
reasoningEffortFallback: reasoningEffort,
|
|
312
310
|
turnMetadata: {
|
|
313
311
|
realtimeTailFlush: {
|
|
314
312
|
source: SESSION_REALTIME_TAIL_SOURCE,
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
SessionRealtimeConflictError,
|
|
16
16
|
type AssertSessionRealtimeOwnerInput,
|
|
17
17
|
} from "./session-realtime";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
lockSessionEventWriteRows,
|
|
20
|
+
lockWorkspaceInferenceControlForAdmission,
|
|
21
|
+
} from "./session-control";
|
|
19
22
|
import { submitHumanPromptInTransaction } from "./session-queue-commands";
|
|
20
23
|
import { mirrorSessionRealtimeContextInTransaction } from "./session-realtime-mirror";
|
|
21
24
|
|
|
@@ -159,6 +162,8 @@ export type SessionRealtimeInboundEntryInput = {
|
|
|
159
162
|
};
|
|
160
163
|
|
|
161
164
|
export type SyncSessionRealtimeLedgerInput = AssertSessionRealtimeOwnerInput & {
|
|
165
|
+
/** Request-scoped callers bound the control prefix wait; lifecycle callers omit it. */
|
|
166
|
+
controlLockTimeoutMs?: number;
|
|
162
167
|
connectionId: string;
|
|
163
168
|
connectionEpoch: number;
|
|
164
169
|
entries?: SessionRealtimeInboundEntryInput[] | undefined;
|
|
@@ -1142,7 +1147,8 @@ async function admitRealtimeDelegationInTransaction(
|
|
|
1142
1147
|
if (!session || session.accountId !== accountId || session.status === "cancelled") {
|
|
1143
1148
|
throw new SessionRealtimeConflictError("REALTIME_NOT_FOUND", "Session not found");
|
|
1144
1149
|
}
|
|
1145
|
-
const
|
|
1150
|
+
const sessionReasoning = ReasoningEffort.parse(session.reasoningEffort);
|
|
1151
|
+
const sessionLatency = LatencyMode.parse(session.latencyMode);
|
|
1146
1152
|
const [latestStarted] = await db
|
|
1147
1153
|
.select({
|
|
1148
1154
|
model: schema.sessionTurns.model,
|
|
@@ -1179,10 +1185,8 @@ async function admitRealtimeDelegationInTransaction(
|
|
|
1179
1185
|
subjectId: input.ownerSubjectId,
|
|
1180
1186
|
subjectLabel: "Realtime",
|
|
1181
1187
|
actor: {
|
|
1182
|
-
type: "
|
|
1188
|
+
type: "human",
|
|
1183
1189
|
subjectId: input.ownerSubjectId,
|
|
1184
|
-
subjectLabel: "Realtime",
|
|
1185
|
-
context: provenance,
|
|
1186
1190
|
},
|
|
1187
1191
|
operationKey: incoming.operationId,
|
|
1188
1192
|
delivery: "steer",
|
|
@@ -1193,15 +1197,12 @@ async function admitRealtimeDelegationInTransaction(
|
|
|
1193
1197
|
text: inputTranscript,
|
|
1194
1198
|
context: incoming.text!,
|
|
1195
1199
|
},
|
|
1200
|
+
mirrorToRealtime: false,
|
|
1196
1201
|
resources: [],
|
|
1197
1202
|
model: latestStarted?.model ?? session.model,
|
|
1198
|
-
reasoningEffort: latestReasoning.success
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
? reasoning.data
|
|
1202
|
-
: "medium",
|
|
1203
|
-
latencyMode: latestLatency.success ? latestLatency.data : "standard",
|
|
1204
|
-
reasoningEffortFallback: reasoning.success ? reasoning.data : "medium",
|
|
1203
|
+
reasoningEffort: latestReasoning.success ? latestReasoning.data : sessionReasoning,
|
|
1204
|
+
latencyMode: latestLatency.success ? latestLatency.data : sessionLatency,
|
|
1205
|
+
reasoningEffortFallback: sessionReasoning,
|
|
1205
1206
|
turnMetadata: {
|
|
1206
1207
|
realtimeDelegation: { ...provenance, inputTranscript },
|
|
1207
1208
|
},
|
|
@@ -1323,8 +1324,17 @@ export async function syncSessionRealtimeLedgerInTransaction(
|
|
|
1323
1324
|
throw new Error("Realtime ledger batch exceeds the server limit");
|
|
1324
1325
|
}
|
|
1325
1326
|
// Realtime sync may admit canonical Steer work. Preserve the global lock
|
|
1326
|
-
// order before owner proof locks the session row.
|
|
1327
|
-
|
|
1327
|
+
// order before owner proof locks the session row. The prefix is shared while
|
|
1328
|
+
// the session branch is active; the delegation Steer below re-enters the same
|
|
1329
|
+
// admission helper and observes this already-held prefix.
|
|
1330
|
+
await lockWorkspaceInferenceControlForAdmission(db, {
|
|
1331
|
+
workspaceId: input.workspaceId,
|
|
1332
|
+
sessionId: input.sessionId,
|
|
1333
|
+
resumePausedBranch: true,
|
|
1334
|
+
...(input.controlLockTimeoutMs !== undefined
|
|
1335
|
+
? { lockTimeoutMs: input.controlLockTimeoutMs }
|
|
1336
|
+
: {}),
|
|
1337
|
+
});
|
|
1328
1338
|
const mode = await assertSessionRealtimeOwnerInTransaction(db, input);
|
|
1329
1339
|
if (mode.connectionEpoch !== input.connectionEpoch) {
|
|
1330
1340
|
throw new SessionRealtimeConflictError(
|
|
@@ -1500,6 +1510,10 @@ export async function syncSessionRealtimeLedgerInTransaction(
|
|
|
1500
1510
|
entry.id,
|
|
1501
1511
|
modelContext,
|
|
1502
1512
|
);
|
|
1513
|
+
// Human prompt admission can mirror a provider-out context row into this
|
|
1514
|
+
// ledger. Continue after that canonical append instead of reusing its
|
|
1515
|
+
// sequence for the next provider-in entry in this batch.
|
|
1516
|
+
nextSequence = await nextLedgerSequence(db, input.realtimeId);
|
|
1503
1517
|
const [linked] = await db
|
|
1504
1518
|
.update(schema.sessionRealtimeEntries)
|
|
1505
1519
|
.set({ turnId: admission.turnId, updatedAt: now })
|
|
@@ -215,7 +215,7 @@ export function renderRealtimeHumanInputRequestContext(input: {
|
|
|
215
215
|
lines.push(" </options>");
|
|
216
216
|
}
|
|
217
217
|
lines.push(
|
|
218
|
-
` <allow_other>${String(question.
|
|
218
|
+
` <allow_other>${String(question.kind !== "text")}</allow_other>`,
|
|
219
219
|
" </question>",
|
|
220
220
|
);
|
|
221
221
|
}
|
package/src/session-tenancy.ts
CHANGED
|
@@ -1,15 +1,93 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
|
+
import {
|
|
4
|
+
SessionTenancyBlocker as SessionTenancyBlockerSchema,
|
|
5
|
+
type SessionTenancyBlocker,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
3
7
|
import {
|
|
4
8
|
rawRows,
|
|
5
9
|
type Database,
|
|
6
10
|
rlsContextForWorkspace,
|
|
11
|
+
withWorkspaceRls,
|
|
7
12
|
withWorkspaceSubjectRls,
|
|
8
13
|
withWorkspaceSubjectSessionActivityRls,
|
|
9
14
|
} from "./database";
|
|
15
|
+
import { nestedPostgresSqlState } from "./persistence-errors";
|
|
16
|
+
|
|
17
|
+
const SESSION_TENANCY_ACTIVATION_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
export class SessionTenancyConflictError extends Error {
|
|
20
|
+
readonly name = "SessionTenancyConflictError";
|
|
21
|
+
constructor(
|
|
22
|
+
readonly reason: "not_quiescent" | "authority_epoch" | "operation_reuse",
|
|
23
|
+
readonly blocker: SessionTenancyBlocker | null = null,
|
|
24
|
+
options?: ErrorOptions,
|
|
25
|
+
) {
|
|
26
|
+
super(
|
|
27
|
+
reason === "not_quiescent"
|
|
28
|
+
? "Session must be fully quiescent before its tenancy can change"
|
|
29
|
+
: reason === "authority_epoch"
|
|
30
|
+
? "Session authority changed before the operation committed"
|
|
31
|
+
: "Session tenancy operation key was reused with different input",
|
|
32
|
+
options,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class SessionTenancyNotActivatedError extends Error {
|
|
38
|
+
readonly name = "SessionTenancyNotActivatedError";
|
|
39
|
+
constructor(options?: ErrorOptions) {
|
|
40
|
+
super("Session tenancy product surface is not activated for this organization", options);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class SessionTenancyAccessError extends Error {
|
|
45
|
+
readonly name = "SessionTenancyAccessError";
|
|
46
|
+
constructor(options?: ErrorOptions) {
|
|
47
|
+
super("Session tenancy target is unavailable", options);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class SessionTenancyInvalidRequestError extends Error {
|
|
52
|
+
readonly name = "SessionTenancyInvalidRequestError";
|
|
53
|
+
constructor(options?: ErrorOptions) {
|
|
54
|
+
super("Session tenancy request is invalid", options);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
59
|
+
return Boolean(value && typeof value === "object");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Extract only the fixed quiescence DETAIL vocabulary, never arbitrary driver detail. */
|
|
63
|
+
export function nestedSessionTenancyBlocker(error: unknown): SessionTenancyBlocker | null {
|
|
64
|
+
const queue: unknown[] = [error];
|
|
65
|
+
const seen = new Set<unknown>();
|
|
66
|
+
while (queue.length > 0 && seen.size < 64) {
|
|
67
|
+
const current = queue.shift();
|
|
68
|
+
if (!isRecord(current) || seen.has(current)) continue;
|
|
69
|
+
seen.add(current);
|
|
70
|
+
for (const key of ["detail", "detailMessage"] as const) {
|
|
71
|
+
const parsed = SessionTenancyBlockerSchema.safeParse(current[key]);
|
|
72
|
+
if (parsed.success) return parsed.data;
|
|
73
|
+
}
|
|
74
|
+
for (const key of ["cause", "original", "driverError", "error", "errors"] as const) {
|
|
75
|
+
const nested = current[key];
|
|
76
|
+
if (Array.isArray(nested)) queue.push(...nested);
|
|
77
|
+
else if (nested !== undefined) queue.push(nested);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
10
82
|
|
|
11
83
|
export type SessionTenancyVisibility = "user_private" | "workspace_shared";
|
|
12
84
|
|
|
85
|
+
export type PrivateSessionCreatePolicy = {
|
|
86
|
+
personalWorkspace: boolean;
|
|
87
|
+
platformAvailable: boolean;
|
|
88
|
+
organizationEnabled: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
13
91
|
export type TransitionSessionVisibilityInput = {
|
|
14
92
|
workspaceId: string;
|
|
15
93
|
sessionId: string;
|
|
@@ -21,6 +99,8 @@ export type TransitionSessionVisibilityInput = {
|
|
|
21
99
|
|
|
22
100
|
export type TransitionSessionVisibilityResult = {
|
|
23
101
|
operationId: string;
|
|
102
|
+
eventId: string | null;
|
|
103
|
+
eventSequence: number | null;
|
|
24
104
|
visibility: SessionTenancyVisibility;
|
|
25
105
|
authorityEpoch: number;
|
|
26
106
|
ownerOrganizationMembershipId: string | null;
|
|
@@ -39,11 +119,14 @@ export type ForkSessionContentInput = {
|
|
|
39
119
|
actorSubjectId: string;
|
|
40
120
|
destinationWorkspaceId: string;
|
|
41
121
|
destinationVisibility: SessionTenancyVisibility;
|
|
122
|
+
workspaceSharedAcknowledged: boolean;
|
|
42
123
|
operationKey: string;
|
|
43
124
|
};
|
|
44
125
|
|
|
45
126
|
export type ForkSessionContentResult = {
|
|
46
127
|
operationId: string;
|
|
128
|
+
eventId: string;
|
|
129
|
+
eventSequence: number;
|
|
47
130
|
sessionId: string;
|
|
48
131
|
workspaceId: string;
|
|
49
132
|
visibility: SessionTenancyVisibility;
|
|
@@ -52,6 +135,178 @@ export type ForkSessionContentResult = {
|
|
|
52
135
|
replay: boolean;
|
|
53
136
|
};
|
|
54
137
|
|
|
138
|
+
export async function sessionTenancyProductActivated(
|
|
139
|
+
db: Database,
|
|
140
|
+
workspaceId: string,
|
|
141
|
+
): Promise<boolean> {
|
|
142
|
+
const { accountId } = await rlsContextForWorkspace(db, workspaceId);
|
|
143
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
144
|
+
const rows = await rawRows<{ activated: boolean }>(
|
|
145
|
+
scopedDb,
|
|
146
|
+
sql`select session_tenancy_product_activated(
|
|
147
|
+
${accountId}::uuid,
|
|
148
|
+
${SESSION_TENANCY_ACTIVATION_VERSION}
|
|
149
|
+
) as activated`,
|
|
150
|
+
);
|
|
151
|
+
return rows[0]?.activated === true;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function getPrivateSessionCreatePolicy(
|
|
156
|
+
db: Database,
|
|
157
|
+
input: { workspaceId: string; actorSubjectId: string },
|
|
158
|
+
): Promise<PrivateSessionCreatePolicy> {
|
|
159
|
+
const { accountId } = await rlsContextForWorkspace(db, input.workspaceId);
|
|
160
|
+
return await withWorkspaceSubjectRls(
|
|
161
|
+
db,
|
|
162
|
+
input.workspaceId,
|
|
163
|
+
input.actorSubjectId,
|
|
164
|
+
async (scopedDb) => {
|
|
165
|
+
const rows = await rawRows<{
|
|
166
|
+
personalWorkspace: boolean;
|
|
167
|
+
platformAvailable: boolean;
|
|
168
|
+
organizationEnabled: boolean;
|
|
169
|
+
}>(
|
|
170
|
+
scopedDb,
|
|
171
|
+
sql`select
|
|
172
|
+
personal_workspace as "personalWorkspace",
|
|
173
|
+
platform_available as "platformAvailable",
|
|
174
|
+
organization_enabled as "organizationEnabled"
|
|
175
|
+
from get_private_session_create_policy(
|
|
176
|
+
${accountId}::uuid,
|
|
177
|
+
${input.workspaceId}::uuid,
|
|
178
|
+
${input.actorSubjectId}
|
|
179
|
+
)`,
|
|
180
|
+
);
|
|
181
|
+
const policy = rows[0];
|
|
182
|
+
if (!policy) throw new SessionTenancyAccessError();
|
|
183
|
+
return policy;
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Open the exact transaction-local trigger capability used by atomic private create. */
|
|
189
|
+
export async function openPrivateSessionCreateCapability(
|
|
190
|
+
db: Database,
|
|
191
|
+
input: { accountId: string; workspaceId: string; sessionId: string; actorSubjectId: string },
|
|
192
|
+
): Promise<{ capabilityId: string; ownerMembershipId: string }> {
|
|
193
|
+
let rows: Array<{ capabilityId: string; ownerMembershipId: string }>;
|
|
194
|
+
try {
|
|
195
|
+
rows = await rawRows<{ capabilityId: string; ownerMembershipId: string }>(
|
|
196
|
+
db,
|
|
197
|
+
sql`select
|
|
198
|
+
capability_id as "capabilityId",
|
|
199
|
+
owner_membership_id as "ownerMembershipId"
|
|
200
|
+
from open_private_session_create_capability(
|
|
201
|
+
${input.accountId}::uuid,
|
|
202
|
+
${input.workspaceId}::uuid,
|
|
203
|
+
${input.sessionId}::uuid,
|
|
204
|
+
${input.actorSubjectId}
|
|
205
|
+
)`,
|
|
206
|
+
);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// The definer raises 55000 only when the organization owner/admin setting
|
|
209
|
+
// does not permit a new private session in this shared workspace; every
|
|
210
|
+
// authority failure remains 42501 and propagates unchanged.
|
|
211
|
+
if (nestedPostgresSqlState(error) === "55000") {
|
|
212
|
+
throw new SessionTenancyNotActivatedError({ cause: error });
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
const capabilityId = rows[0]?.capabilityId;
|
|
217
|
+
const ownerMembershipId = rows[0]?.ownerMembershipId;
|
|
218
|
+
if (!capabilityId || !ownerMembershipId) {
|
|
219
|
+
throw new Error("Private session create capability was not returned");
|
|
220
|
+
}
|
|
221
|
+
return { capabilityId, ownerMembershipId };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Open the exact transaction-local capability for one private child insert. */
|
|
225
|
+
export async function openPrivateChildSessionCreateCapability(
|
|
226
|
+
db: Database,
|
|
227
|
+
input: {
|
|
228
|
+
accountId: string;
|
|
229
|
+
workspaceId: string;
|
|
230
|
+
sessionId: string;
|
|
231
|
+
parentSessionId: string;
|
|
232
|
+
actorTurnId: string;
|
|
233
|
+
actorAttemptId: string;
|
|
234
|
+
actorExecutionGeneration: number;
|
|
235
|
+
},
|
|
236
|
+
): Promise<{ capabilityId: string; ownerMembershipId: string; ownerSubjectId: string }> {
|
|
237
|
+
const rows = await rawRows<{
|
|
238
|
+
capabilityId: string;
|
|
239
|
+
ownerMembershipId: string;
|
|
240
|
+
ownerSubjectId: string;
|
|
241
|
+
}>(
|
|
242
|
+
db,
|
|
243
|
+
sql`select
|
|
244
|
+
capability_id as "capabilityId",
|
|
245
|
+
owner_membership_id as "ownerMembershipId",
|
|
246
|
+
owner_subject_id as "ownerSubjectId"
|
|
247
|
+
from open_private_child_session_create_capability(
|
|
248
|
+
${input.accountId}::uuid,
|
|
249
|
+
${input.workspaceId}::uuid,
|
|
250
|
+
${input.sessionId}::uuid,
|
|
251
|
+
${input.parentSessionId}::uuid,
|
|
252
|
+
${input.actorTurnId}::uuid,
|
|
253
|
+
${input.actorAttemptId}::uuid,
|
|
254
|
+
${input.actorExecutionGeneration}::integer
|
|
255
|
+
)`,
|
|
256
|
+
);
|
|
257
|
+
const capabilityId = rows[0]?.capabilityId;
|
|
258
|
+
const ownerMembershipId = rows[0]?.ownerMembershipId;
|
|
259
|
+
const ownerSubjectId = rows[0]?.ownerSubjectId;
|
|
260
|
+
if (!capabilityId || !ownerMembershipId || !ownerSubjectId) {
|
|
261
|
+
throw new Error("Private child session create capability was not returned");
|
|
262
|
+
}
|
|
263
|
+
return { capabilityId, ownerMembershipId, ownerSubjectId };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function closePrivateSessionCreateCapability(
|
|
267
|
+
db: Database,
|
|
268
|
+
capabilityId: string,
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
await db.execute(sql`select close_private_session_create_capability(${capabilityId}::uuid)`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function assertSessionTenancyProductActivated(
|
|
274
|
+
db: Database,
|
|
275
|
+
workspaceId: string,
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
if (!(await sessionTenancyProductActivated(db, workspaceId))) {
|
|
278
|
+
throw new SessionTenancyNotActivatedError();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function mapSessionTenancyPersistenceError(
|
|
283
|
+
error: unknown,
|
|
284
|
+
options: { authorityEpochConflict: boolean },
|
|
285
|
+
): never {
|
|
286
|
+
const state = nestedPostgresSqlState(error);
|
|
287
|
+
if (state === "55P03") {
|
|
288
|
+
throw new SessionTenancyConflictError("not_quiescent", nestedSessionTenancyBlocker(error), {
|
|
289
|
+
cause: error,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (state === "40001" && options.authorityEpochConflict) {
|
|
293
|
+
throw new SessionTenancyConflictError("authority_epoch", null, { cause: error });
|
|
294
|
+
}
|
|
295
|
+
if (state === "23505") {
|
|
296
|
+
throw new SessionTenancyConflictError("operation_reuse", null, { cause: error });
|
|
297
|
+
}
|
|
298
|
+
if (state === "55000") {
|
|
299
|
+
throw new SessionTenancyNotActivatedError({ cause: error });
|
|
300
|
+
}
|
|
301
|
+
if (state === "42501" || state === "P0002") {
|
|
302
|
+
throw new SessionTenancyAccessError({ cause: error });
|
|
303
|
+
}
|
|
304
|
+
if (state === "22023") {
|
|
305
|
+
throw new SessionTenancyInvalidRequestError({ cause: error });
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
|
|
55
310
|
export function canonicalSessionVisibilityTransitionHash(
|
|
56
311
|
input: Pick<
|
|
57
312
|
TransitionSessionVisibilityInput,
|
|
@@ -73,17 +328,32 @@ export function canonicalSessionVisibilityTransitionHash(
|
|
|
73
328
|
export function canonicalSessionForkHash(
|
|
74
329
|
input: Pick<
|
|
75
330
|
ForkSessionContentInput,
|
|
76
|
-
|
|
331
|
+
| "sourceSessionId"
|
|
332
|
+
| "destinationWorkspaceId"
|
|
333
|
+
| "destinationVisibility"
|
|
334
|
+
| "workspaceSharedAcknowledged"
|
|
77
335
|
>,
|
|
78
336
|
): string {
|
|
337
|
+
const legacyCompatiblePrivateRequest =
|
|
338
|
+
input.destinationVisibility === "user_private" && !input.workspaceSharedAcknowledged;
|
|
79
339
|
return createHash("sha256")
|
|
80
340
|
.update(
|
|
81
|
-
JSON.stringify(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
341
|
+
JSON.stringify(
|
|
342
|
+
legacyCompatiblePrivateRequest
|
|
343
|
+
? {
|
|
344
|
+
version: 1,
|
|
345
|
+
sourceSessionId: input.sourceSessionId,
|
|
346
|
+
destinationWorkspaceId: input.destinationWorkspaceId,
|
|
347
|
+
destinationVisibility: input.destinationVisibility,
|
|
348
|
+
}
|
|
349
|
+
: {
|
|
350
|
+
version: 2,
|
|
351
|
+
sourceSessionId: input.sourceSessionId,
|
|
352
|
+
destinationWorkspaceId: input.destinationWorkspaceId,
|
|
353
|
+
destinationVisibility: input.destinationVisibility,
|
|
354
|
+
workspaceSharedAcknowledged: input.workspaceSharedAcknowledged,
|
|
355
|
+
},
|
|
356
|
+
),
|
|
87
357
|
)
|
|
88
358
|
.digest("hex");
|
|
89
359
|
}
|
|
@@ -98,27 +368,33 @@ export async function transitionSessionVisibility(
|
|
|
98
368
|
if (!input.operationKey.trim()) throw new Error("operationKey must not be empty");
|
|
99
369
|
const requestHash = canonicalSessionVisibilityTransitionHash(input);
|
|
100
370
|
const { accountId } = await rlsContextForWorkspace(db, input.workspaceId);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
371
|
+
await assertSessionTenancyProductActivated(db, input.workspaceId);
|
|
372
|
+
try {
|
|
373
|
+
return await withWorkspaceSubjectSessionActivityRls(
|
|
374
|
+
db,
|
|
375
|
+
input.workspaceId,
|
|
376
|
+
input.actorSubjectId,
|
|
377
|
+
async (scopedDb) => {
|
|
378
|
+
const rows = await rawRows<{
|
|
379
|
+
operationId: string;
|
|
380
|
+
eventId: string | null;
|
|
381
|
+
eventSequence: number | null;
|
|
382
|
+
visibility: SessionTenancyVisibility;
|
|
383
|
+
authorityEpoch: number;
|
|
384
|
+
ownerOrganizationMembershipId: string | null;
|
|
385
|
+
changed: boolean;
|
|
386
|
+
replay: boolean;
|
|
387
|
+
interruptedAttemptCount: number;
|
|
388
|
+
cancelledTurnCount: number;
|
|
389
|
+
cancelledUpdateCount: number;
|
|
390
|
+
pausedGoalCount: number;
|
|
391
|
+
revokedGrantCount: number;
|
|
392
|
+
}>(
|
|
393
|
+
scopedDb,
|
|
394
|
+
sql`select
|
|
121
395
|
operation_id as "operationId",
|
|
396
|
+
event_id as "eventId",
|
|
397
|
+
event_sequence as "eventSequence",
|
|
122
398
|
visibility,
|
|
123
399
|
authority_epoch as "authorityEpoch",
|
|
124
400
|
owner_organization_membership_id as "ownerOrganizationMembershipId",
|
|
@@ -137,14 +413,20 @@ export async function transitionSessionVisibility(
|
|
|
137
413
|
${input.targetVisibility},
|
|
138
414
|
${input.expectedAuthorityEpoch},
|
|
139
415
|
${input.operationKey},
|
|
140
|
-
${requestHash}
|
|
416
|
+
${requestHash},
|
|
417
|
+
${SESSION_TENANCY_ACTIVATION_VERSION}
|
|
141
418
|
)`,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
419
|
+
);
|
|
420
|
+
const result = rows[0];
|
|
421
|
+
if (!result) throw new Error("Session visibility transition returned no result");
|
|
422
|
+
return result;
|
|
423
|
+
},
|
|
424
|
+
undefined,
|
|
425
|
+
"none",
|
|
426
|
+
);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
mapSessionTenancyPersistenceError(error, { authorityEpochConflict: true });
|
|
429
|
+
}
|
|
148
430
|
}
|
|
149
431
|
|
|
150
432
|
export async function forkSessionContent(
|
|
@@ -152,17 +434,24 @@ export async function forkSessionContent(
|
|
|
152
434
|
input: ForkSessionContentInput,
|
|
153
435
|
): Promise<ForkSessionContentResult> {
|
|
154
436
|
if (!input.operationKey.trim()) throw new Error("operationKey must not be empty");
|
|
437
|
+
if (input.destinationWorkspaceId !== input.sourceWorkspaceId) {
|
|
438
|
+
throw new Error("The first session fork contract is same-workspace only");
|
|
439
|
+
}
|
|
155
440
|
const { accountId } = await rlsContextForWorkspace(db, input.sourceWorkspaceId);
|
|
441
|
+
await assertSessionTenancyProductActivated(db, input.sourceWorkspaceId);
|
|
156
442
|
const requestHash = canonicalSessionForkHash(input);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
443
|
+
try {
|
|
444
|
+
return await withWorkspaceSubjectRls(
|
|
445
|
+
db,
|
|
446
|
+
input.sourceWorkspaceId,
|
|
447
|
+
input.actorSubjectId,
|
|
448
|
+
async (scopedDb) => {
|
|
449
|
+
const rows = await rawRows<ForkSessionContentResult>(
|
|
450
|
+
scopedDb,
|
|
451
|
+
sql`select
|
|
165
452
|
operation_id as "operationId",
|
|
453
|
+
event_id as "eventId",
|
|
454
|
+
event_sequence as "eventSequence",
|
|
166
455
|
session_id as "sessionId",
|
|
167
456
|
workspace_id as "workspaceId",
|
|
168
457
|
visibility,
|
|
@@ -176,13 +465,85 @@ export async function forkSessionContent(
|
|
|
176
465
|
${input.actorSubjectId},
|
|
177
466
|
${input.destinationWorkspaceId}::uuid,
|
|
178
467
|
${input.destinationVisibility},
|
|
468
|
+
${input.workspaceSharedAcknowledged},
|
|
179
469
|
${input.operationKey},
|
|
180
|
-
${requestHash}
|
|
470
|
+
${requestHash},
|
|
471
|
+
${SESSION_TENANCY_ACTIVATION_VERSION}
|
|
181
472
|
)`,
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
473
|
+
);
|
|
474
|
+
const result = rows[0];
|
|
475
|
+
if (!result) throw new Error("Session fork returned no result");
|
|
476
|
+
return result;
|
|
477
|
+
},
|
|
478
|
+
undefined,
|
|
479
|
+
"none",
|
|
480
|
+
);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
// A private destination inside a shared workspace carries the same
|
|
483
|
+
// organization owner/admin product decision the create path enforces
|
|
484
|
+
// (migration 0323), and the fork routine raises the same 55000. Surface it
|
|
485
|
+
// as the not-activated product state rather than an opaque database error.
|
|
486
|
+
if (nestedPostgresSqlState(error) === "55000") {
|
|
487
|
+
throw new SessionTenancyNotActivatedError({ cause: error });
|
|
488
|
+
}
|
|
489
|
+
mapSessionTenancyPersistenceError(error, { authorityEpochConflict: false });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Recover one exact committed fork before mutable source-session authorization.
|
|
495
|
+
*
|
|
496
|
+
* The database capability requires the authenticated actor's active workspace
|
|
497
|
+
* authority and matches the complete actor/workspace/source/key/request-hash
|
|
498
|
+
* tuple. A fresh key returns null; changed intent raises the ordinary tenancy
|
|
499
|
+
* idempotency conflict. This is receipt recovery, not session discovery.
|
|
500
|
+
*/
|
|
501
|
+
export async function replayAppliedSessionFork(
|
|
502
|
+
db: Database,
|
|
503
|
+
input: ForkSessionContentInput,
|
|
504
|
+
): Promise<ForkSessionContentResult | null> {
|
|
505
|
+
if (!input.operationKey.trim()) throw new Error("operationKey must not be empty");
|
|
506
|
+
if (input.destinationWorkspaceId !== input.sourceWorkspaceId) {
|
|
507
|
+
throw new Error("The first session fork contract is same-workspace only");
|
|
508
|
+
}
|
|
509
|
+
const { accountId } = await rlsContextForWorkspace(db, input.sourceWorkspaceId);
|
|
510
|
+
await assertSessionTenancyProductActivated(db, input.sourceWorkspaceId);
|
|
511
|
+
const requestHash = canonicalSessionForkHash(input);
|
|
512
|
+
try {
|
|
513
|
+
return await withWorkspaceSubjectRls(
|
|
514
|
+
db,
|
|
515
|
+
input.sourceWorkspaceId,
|
|
516
|
+
input.actorSubjectId,
|
|
517
|
+
async (scopedDb) => {
|
|
518
|
+
const rows = await rawRows<ForkSessionContentResult>(
|
|
519
|
+
scopedDb,
|
|
520
|
+
sql`select
|
|
521
|
+
operation_id as "operationId",
|
|
522
|
+
event_id as "eventId",
|
|
523
|
+
event_sequence as "eventSequence",
|
|
524
|
+
session_id as "sessionId",
|
|
525
|
+
workspace_id as "workspaceId",
|
|
526
|
+
visibility,
|
|
527
|
+
authority_epoch as "authorityEpoch",
|
|
528
|
+
copied_history_item_count as "copiedHistoryItemCount",
|
|
529
|
+
replay
|
|
530
|
+
from replay_applied_session_fork(
|
|
531
|
+
${accountId}::uuid,
|
|
532
|
+
${input.sourceWorkspaceId}::uuid,
|
|
533
|
+
${input.sourceSessionId}::uuid,
|
|
534
|
+
${input.actorSubjectId},
|
|
535
|
+
${input.destinationWorkspaceId}::uuid,
|
|
536
|
+
${input.destinationVisibility},
|
|
537
|
+
${input.workspaceSharedAcknowledged},
|
|
538
|
+
${input.operationKey},
|
|
539
|
+
${requestHash},
|
|
540
|
+
${SESSION_TENANCY_ACTIVATION_VERSION}
|
|
541
|
+
)`,
|
|
542
|
+
);
|
|
543
|
+
return rows[0] ?? null;
|
|
544
|
+
},
|
|
545
|
+
);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
mapSessionTenancyPersistenceError(error, { authorityEpochConflict: false });
|
|
548
|
+
}
|
|
188
549
|
}
|