@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
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SessionBackgroundCommand,
|
|
3
|
+
SessionBackgroundCommandActivity,
|
|
4
|
+
} from "@opengeni/contracts";
|
|
5
|
+
import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
|
6
|
+
|
|
7
|
+
import type { Database } from "./database";
|
|
8
|
+
import { withRlsContext } from "./database";
|
|
9
|
+
import * as schema from "./schema";
|
|
10
|
+
|
|
11
|
+
export type ConnectedMachineBackgroundCommandProof = {
|
|
12
|
+
outcome: "exited" | "lost";
|
|
13
|
+
exitCode: number | null;
|
|
14
|
+
reason: string;
|
|
15
|
+
observedAt: Date;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ConnectedMachineBackgroundCommandClaim = {
|
|
19
|
+
commandId: string;
|
|
20
|
+
accountId: string;
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
claimId: string;
|
|
24
|
+
state: "running" | "stopping";
|
|
25
|
+
controlWorkspaceId: string;
|
|
26
|
+
enrollmentId: string;
|
|
27
|
+
connectionInstanceId: string;
|
|
28
|
+
opId: string;
|
|
29
|
+
reconcileAttempts: number;
|
|
30
|
+
proof: ConnectedMachineBackgroundCommandProof | null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ConnectedMachineBackgroundCommandClaimRow = {
|
|
34
|
+
accountId: string;
|
|
35
|
+
workspaceId: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
commandId: string;
|
|
38
|
+
claimId: string;
|
|
39
|
+
commandState: string;
|
|
40
|
+
controlWorkspaceId: string;
|
|
41
|
+
enrollmentId: string;
|
|
42
|
+
connectionInstanceId: string;
|
|
43
|
+
opId: string;
|
|
44
|
+
reconcileAttempts: number | string;
|
|
45
|
+
reconcileProofOutcome: string | null;
|
|
46
|
+
reconcileProofExitCode: number | string | null;
|
|
47
|
+
reconcileProofReason: string | null;
|
|
48
|
+
reconcileProofObservedAt: Date | string | null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function commandPreview(value: string): string {
|
|
52
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
53
|
+
return Array.from(normalized).slice(0, 512).join("");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mapCommand(
|
|
57
|
+
row: typeof schema.sessionBackgroundCommands.$inferSelect,
|
|
58
|
+
): SessionBackgroundCommand {
|
|
59
|
+
return {
|
|
60
|
+
id: row.id,
|
|
61
|
+
workspaceId: row.workspaceId,
|
|
62
|
+
sessionId: row.sessionId,
|
|
63
|
+
provider: row.provider,
|
|
64
|
+
state: row.state,
|
|
65
|
+
commandPreview: row.commandPreview,
|
|
66
|
+
cancelRequestedAt: row.cancelRequestedAt?.toISOString() ?? null,
|
|
67
|
+
exitCode: row.exitCode ?? null,
|
|
68
|
+
settlementReason: row.settlementReason ?? null,
|
|
69
|
+
startedAt: row.startedAt.toISOString(),
|
|
70
|
+
settledAt: row.settledAt?.toISOString() ?? null,
|
|
71
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function backgroundCommandActivityForSessions(
|
|
76
|
+
db: Database,
|
|
77
|
+
input: { accountId: string; workspaceId: string; sessionIds: string[] },
|
|
78
|
+
): Promise<Map<string, SessionBackgroundCommandActivity>> {
|
|
79
|
+
if (input.sessionIds.length === 0) return new Map();
|
|
80
|
+
return await withRlsContext(
|
|
81
|
+
db,
|
|
82
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
83
|
+
async (scopedDb) => {
|
|
84
|
+
const rows = await scopedDb
|
|
85
|
+
.select({
|
|
86
|
+
sessionId: schema.sessionBackgroundCommands.sessionId,
|
|
87
|
+
count: sql<number>`count(*)::int`,
|
|
88
|
+
stoppingCount: sql<number>`count(*) filter (where ${schema.sessionBackgroundCommands.state} = 'stopping')::int`,
|
|
89
|
+
})
|
|
90
|
+
.from(schema.sessionBackgroundCommands)
|
|
91
|
+
.where(
|
|
92
|
+
and(
|
|
93
|
+
eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
94
|
+
inArray(schema.sessionBackgroundCommands.sessionId, input.sessionIds),
|
|
95
|
+
inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
.groupBy(schema.sessionBackgroundCommands.sessionId);
|
|
99
|
+
return new Map(
|
|
100
|
+
rows.map((row) => [
|
|
101
|
+
row.sessionId,
|
|
102
|
+
{
|
|
103
|
+
state: Number(row.stoppingCount) > 0 ? ("stopping" as const) : ("running" as const),
|
|
104
|
+
count: Number(row.count),
|
|
105
|
+
},
|
|
106
|
+
]),
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function listSessionBackgroundCommands(
|
|
113
|
+
db: Database,
|
|
114
|
+
input: { accountId: string; workspaceId: string; sessionId: string },
|
|
115
|
+
): Promise<SessionBackgroundCommand[]> {
|
|
116
|
+
return await withRlsContext(
|
|
117
|
+
db,
|
|
118
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
119
|
+
async (scopedDb) => {
|
|
120
|
+
const rows = await scopedDb
|
|
121
|
+
.select()
|
|
122
|
+
.from(schema.sessionBackgroundCommands)
|
|
123
|
+
.where(
|
|
124
|
+
and(
|
|
125
|
+
eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
126
|
+
eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
.orderBy(
|
|
130
|
+
desc(schema.sessionBackgroundCommands.startedAt),
|
|
131
|
+
desc(schema.sessionBackgroundCommands.id),
|
|
132
|
+
)
|
|
133
|
+
.limit(1000);
|
|
134
|
+
return rows.map(mapCommand);
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @internal Caller must already hold the canonical session/control fence. */
|
|
140
|
+
export async function insertManagedSessionBackgroundCommandInTransaction(
|
|
141
|
+
db: Database,
|
|
142
|
+
input: {
|
|
143
|
+
accountId: string;
|
|
144
|
+
workspaceId: string;
|
|
145
|
+
sessionId: string;
|
|
146
|
+
commandId: string;
|
|
147
|
+
retainedProcessId: string;
|
|
148
|
+
command: string;
|
|
149
|
+
},
|
|
150
|
+
): Promise<SessionBackgroundCommand> {
|
|
151
|
+
const [process] = await db
|
|
152
|
+
.select({
|
|
153
|
+
accountId: schema.sandboxRetainedProcesses.accountId,
|
|
154
|
+
workspaceId: schema.sandboxRetainedProcesses.workspaceId,
|
|
155
|
+
sessionId: schema.sandboxRetainedProcesses.sessionId,
|
|
156
|
+
state: schema.sandboxRetainedProcesses.state,
|
|
157
|
+
})
|
|
158
|
+
.from(schema.sandboxRetainedProcesses)
|
|
159
|
+
.where(eq(schema.sandboxRetainedProcesses.id, input.retainedProcessId))
|
|
160
|
+
.for("update")
|
|
161
|
+
.limit(1);
|
|
162
|
+
if (
|
|
163
|
+
!process ||
|
|
164
|
+
process.accountId !== input.accountId ||
|
|
165
|
+
process.workspaceId !== input.workspaceId ||
|
|
166
|
+
process.sessionId !== input.sessionId ||
|
|
167
|
+
process.state !== "active"
|
|
168
|
+
) {
|
|
169
|
+
throw new Error("Managed background command requires its exact active retained process");
|
|
170
|
+
}
|
|
171
|
+
const [row] = await db
|
|
172
|
+
.insert(schema.sessionBackgroundCommands)
|
|
173
|
+
.values({
|
|
174
|
+
id: input.commandId,
|
|
175
|
+
accountId: input.accountId,
|
|
176
|
+
workspaceId: input.workspaceId,
|
|
177
|
+
sessionId: input.sessionId,
|
|
178
|
+
provider: "managed",
|
|
179
|
+
state: "running",
|
|
180
|
+
retainedProcessId: input.retainedProcessId,
|
|
181
|
+
commandPreview: commandPreview(input.command),
|
|
182
|
+
})
|
|
183
|
+
.onConflictDoUpdate({
|
|
184
|
+
target: schema.sessionBackgroundCommands.retainedProcessId,
|
|
185
|
+
targetWhere: sql`${schema.sessionBackgroundCommands.retainedProcessId} is not null`,
|
|
186
|
+
set: { updatedAt: new Date() },
|
|
187
|
+
})
|
|
188
|
+
.returning();
|
|
189
|
+
if (!row) throw new Error("Managed background command adoption returned no row");
|
|
190
|
+
if (
|
|
191
|
+
row.accountId !== input.accountId ||
|
|
192
|
+
row.workspaceId !== input.workspaceId ||
|
|
193
|
+
row.sessionId !== input.sessionId ||
|
|
194
|
+
row.provider !== "managed" ||
|
|
195
|
+
row.retainedProcessId !== input.retainedProcessId
|
|
196
|
+
) {
|
|
197
|
+
throw new Error("Managed background command adoption conflicted with another identity");
|
|
198
|
+
}
|
|
199
|
+
return mapCommand(row);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** @internal Caller must already hold the canonical exact-attempt fence. */
|
|
203
|
+
export async function insertConnectedMachineSessionBackgroundCommandInTransaction(
|
|
204
|
+
db: Database,
|
|
205
|
+
input: {
|
|
206
|
+
accountId: string;
|
|
207
|
+
workspaceId: string;
|
|
208
|
+
sessionId: string;
|
|
209
|
+
commandId: string;
|
|
210
|
+
controlWorkspaceId: string;
|
|
211
|
+
enrollmentId: string;
|
|
212
|
+
connectionInstanceId: string;
|
|
213
|
+
opId: string;
|
|
214
|
+
command: string;
|
|
215
|
+
},
|
|
216
|
+
): Promise<SessionBackgroundCommand> {
|
|
217
|
+
const [row] = await db
|
|
218
|
+
.insert(schema.sessionBackgroundCommands)
|
|
219
|
+
.values({
|
|
220
|
+
id: input.commandId,
|
|
221
|
+
accountId: input.accountId,
|
|
222
|
+
workspaceId: input.workspaceId,
|
|
223
|
+
sessionId: input.sessionId,
|
|
224
|
+
provider: "connected_machine",
|
|
225
|
+
state: "running",
|
|
226
|
+
controlWorkspaceId: input.controlWorkspaceId,
|
|
227
|
+
enrollmentId: input.enrollmentId,
|
|
228
|
+
connectionInstanceId: input.connectionInstanceId,
|
|
229
|
+
opId: input.opId,
|
|
230
|
+
commandPreview: commandPreview(input.command),
|
|
231
|
+
})
|
|
232
|
+
.onConflictDoUpdate({
|
|
233
|
+
target: [
|
|
234
|
+
schema.sessionBackgroundCommands.controlWorkspaceId,
|
|
235
|
+
schema.sessionBackgroundCommands.enrollmentId,
|
|
236
|
+
schema.sessionBackgroundCommands.connectionInstanceId,
|
|
237
|
+
schema.sessionBackgroundCommands.opId,
|
|
238
|
+
],
|
|
239
|
+
targetWhere: sql`${schema.sessionBackgroundCommands.provider} = 'connected_machine'`,
|
|
240
|
+
set: { updatedAt: new Date() },
|
|
241
|
+
})
|
|
242
|
+
.returning();
|
|
243
|
+
if (!row) throw new Error("Connected Machine background command adoption returned no row");
|
|
244
|
+
if (
|
|
245
|
+
row.accountId !== input.accountId ||
|
|
246
|
+
row.workspaceId !== input.workspaceId ||
|
|
247
|
+
row.sessionId !== input.sessionId ||
|
|
248
|
+
row.provider !== "connected_machine" ||
|
|
249
|
+
row.controlWorkspaceId !== input.controlWorkspaceId ||
|
|
250
|
+
row.enrollmentId !== input.enrollmentId ||
|
|
251
|
+
row.connectionInstanceId !== input.connectionInstanceId ||
|
|
252
|
+
row.opId !== input.opId
|
|
253
|
+
) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
"Connected Machine background command adoption conflicted with another identity",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return mapCommand(row);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Claim exact Connected Machine locators globally for provider reconciliation.
|
|
262
|
+
* The SECURITY DEFINER SQL function owns the bounded SKIP LOCKED inventory;
|
|
263
|
+
* claim expiry is coordination recovery only and never implies command loss. */
|
|
264
|
+
export async function claimConnectedMachineSessionBackgroundCommands(
|
|
265
|
+
db: Database,
|
|
266
|
+
input: { claimId: string; limit: number; claimTtlMs: number },
|
|
267
|
+
): Promise<ConnectedMachineBackgroundCommandClaim[]> {
|
|
268
|
+
if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100) {
|
|
269
|
+
throw new Error("Connected command reconciliation limit must be between 1 and 100");
|
|
270
|
+
}
|
|
271
|
+
if (
|
|
272
|
+
!Number.isSafeInteger(input.claimTtlMs) ||
|
|
273
|
+
input.claimTtlMs < 0 ||
|
|
274
|
+
input.claimTtlMs > 3_600_000
|
|
275
|
+
) {
|
|
276
|
+
throw new Error("Connected command reconciliation claim TTL is invalid");
|
|
277
|
+
}
|
|
278
|
+
const rows = await db.execute<ConnectedMachineBackgroundCommandClaimRow>(sql`
|
|
279
|
+
select account_id as "accountId",
|
|
280
|
+
workspace_id as "workspaceId",
|
|
281
|
+
session_id as "sessionId",
|
|
282
|
+
command_id as "commandId",
|
|
283
|
+
claim_id as "claimId",
|
|
284
|
+
command_state as "commandState",
|
|
285
|
+
control_workspace_id as "controlWorkspaceId",
|
|
286
|
+
enrollment_id as "enrollmentId",
|
|
287
|
+
connection_instance_id as "connectionInstanceId",
|
|
288
|
+
op_id as "opId",
|
|
289
|
+
reconcile_attempts as "reconcileAttempts",
|
|
290
|
+
reconcile_proof_outcome as "reconcileProofOutcome",
|
|
291
|
+
reconcile_proof_exit_code as "reconcileProofExitCode",
|
|
292
|
+
reconcile_proof_reason as "reconcileProofReason",
|
|
293
|
+
reconcile_proof_observed_at as "reconcileProofObservedAt"
|
|
294
|
+
from opengeni_private.claim_connected_machine_background_commands(
|
|
295
|
+
${input.claimId}::uuid, ${input.limit}::integer, ${input.claimTtlMs}::bigint
|
|
296
|
+
)
|
|
297
|
+
`);
|
|
298
|
+
return rows.map((row: ConnectedMachineBackgroundCommandClaimRow) => {
|
|
299
|
+
if (row.commandState !== "running" && row.commandState !== "stopping") {
|
|
300
|
+
throw new Error(`Connected command claim returned invalid state ${row.commandState}`);
|
|
301
|
+
}
|
|
302
|
+
const proof =
|
|
303
|
+
row.reconcileProofOutcome === null
|
|
304
|
+
? null
|
|
305
|
+
: row.reconcileProofOutcome === "exited" || row.reconcileProofOutcome === "lost"
|
|
306
|
+
? {
|
|
307
|
+
outcome: row.reconcileProofOutcome,
|
|
308
|
+
exitCode:
|
|
309
|
+
row.reconcileProofExitCode === null ? null : Number(row.reconcileProofExitCode),
|
|
310
|
+
reason: row.reconcileProofReason!,
|
|
311
|
+
observedAt:
|
|
312
|
+
row.reconcileProofObservedAt instanceof Date
|
|
313
|
+
? row.reconcileProofObservedAt
|
|
314
|
+
: new Date(row.reconcileProofObservedAt!),
|
|
315
|
+
}
|
|
316
|
+
: (() => {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Connected command claim returned invalid proof ${row.reconcileProofOutcome}`,
|
|
319
|
+
);
|
|
320
|
+
})();
|
|
321
|
+
return {
|
|
322
|
+
commandId: row.commandId,
|
|
323
|
+
accountId: row.accountId,
|
|
324
|
+
workspaceId: row.workspaceId,
|
|
325
|
+
sessionId: row.sessionId,
|
|
326
|
+
claimId: row.claimId,
|
|
327
|
+
state: row.commandState,
|
|
328
|
+
controlWorkspaceId: row.controlWorkspaceId,
|
|
329
|
+
enrollmentId: row.enrollmentId,
|
|
330
|
+
connectionInstanceId: row.connectionInstanceId,
|
|
331
|
+
opId: row.opId,
|
|
332
|
+
reconcileAttempts: Number(row.reconcileAttempts),
|
|
333
|
+
proof,
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function connectedClaimIdentityWhere(claim: ConnectedMachineBackgroundCommandClaim) {
|
|
339
|
+
return and(
|
|
340
|
+
eq(schema.sessionBackgroundCommands.id, claim.commandId),
|
|
341
|
+
eq(schema.sessionBackgroundCommands.accountId, claim.accountId),
|
|
342
|
+
eq(schema.sessionBackgroundCommands.workspaceId, claim.workspaceId),
|
|
343
|
+
eq(schema.sessionBackgroundCommands.sessionId, claim.sessionId),
|
|
344
|
+
eq(schema.sessionBackgroundCommands.provider, "connected_machine"),
|
|
345
|
+
eq(schema.sessionBackgroundCommands.controlWorkspaceId, claim.controlWorkspaceId),
|
|
346
|
+
eq(schema.sessionBackgroundCommands.enrollmentId, claim.enrollmentId),
|
|
347
|
+
eq(schema.sessionBackgroundCommands.connectionInstanceId, claim.connectionInstanceId),
|
|
348
|
+
eq(schema.sessionBackgroundCommands.opId, claim.opId),
|
|
349
|
+
eq(schema.sessionBackgroundCommands.reconcileClaimId, claim.claimId),
|
|
350
|
+
inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Checkpoint one exact terminal provider observation before lifecycle settlement.
|
|
355
|
+
* A retry may reuse an identical proof; a divergent proof is a hard identity fault. */
|
|
356
|
+
export async function recordConnectedMachineBackgroundCommandProof(
|
|
357
|
+
db: Database,
|
|
358
|
+
input: {
|
|
359
|
+
claim: ConnectedMachineBackgroundCommandClaim;
|
|
360
|
+
proof: ConnectedMachineBackgroundCommandProof;
|
|
361
|
+
},
|
|
362
|
+
): Promise<void> {
|
|
363
|
+
const reason = input.proof.reason.trim().slice(0, 512);
|
|
364
|
+
if (!reason) throw new Error("Connected command proof reason must not be empty");
|
|
365
|
+
if (input.proof.outcome === "exited" && input.proof.exitCode === null) {
|
|
366
|
+
throw new Error("Connected command exit proof requires an exit code");
|
|
367
|
+
}
|
|
368
|
+
if (input.proof.outcome === "lost" && input.proof.exitCode !== null) {
|
|
369
|
+
throw new Error("Connected command loss proof cannot carry an exit code");
|
|
370
|
+
}
|
|
371
|
+
await withRlsContext(
|
|
372
|
+
db,
|
|
373
|
+
{ accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
|
|
374
|
+
async (scopedDb) => {
|
|
375
|
+
const [current] = await scopedDb
|
|
376
|
+
.select()
|
|
377
|
+
.from(schema.sessionBackgroundCommands)
|
|
378
|
+
.where(connectedClaimIdentityWhere(input.claim))
|
|
379
|
+
.for("update")
|
|
380
|
+
.limit(1);
|
|
381
|
+
if (!current) throw new Error("Connected command proof was fenced by newer lifecycle state");
|
|
382
|
+
if (current.reconcileProofOutcome !== null) {
|
|
383
|
+
const observedAt = current.reconcileProofObservedAt?.getTime() ?? null;
|
|
384
|
+
if (
|
|
385
|
+
current.reconcileProofOutcome !== input.proof.outcome ||
|
|
386
|
+
current.reconcileProofExitCode !== input.proof.exitCode ||
|
|
387
|
+
current.reconcileProofReason !== reason ||
|
|
388
|
+
observedAt !== input.proof.observedAt.getTime()
|
|
389
|
+
) {
|
|
390
|
+
throw new Error("Connected command reconciliation proof conflicts with durable proof");
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const rows = await scopedDb
|
|
395
|
+
.update(schema.sessionBackgroundCommands)
|
|
396
|
+
.set({
|
|
397
|
+
reconcileProofOutcome: input.proof.outcome,
|
|
398
|
+
reconcileProofExitCode: input.proof.exitCode,
|
|
399
|
+
reconcileProofReason: reason,
|
|
400
|
+
reconcileProofObservedAt: input.proof.observedAt,
|
|
401
|
+
lastReconcileOutcome: `proof_${input.proof.outcome}`,
|
|
402
|
+
updatedAt: new Date(),
|
|
403
|
+
})
|
|
404
|
+
.where(connectedClaimIdentityWhere(input.claim))
|
|
405
|
+
.returning({ id: schema.sessionBackgroundCommands.id });
|
|
406
|
+
if (rows.length !== 1) {
|
|
407
|
+
throw new Error("Connected command proof lost its exact claim during checkpoint");
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function deferConnectedMachineBackgroundCommandReconciliation(
|
|
414
|
+
db: Database,
|
|
415
|
+
input: {
|
|
416
|
+
claim: ConnectedMachineBackgroundCommandClaim;
|
|
417
|
+
outcome: string;
|
|
418
|
+
retryAfterMs: number;
|
|
419
|
+
},
|
|
420
|
+
): Promise<boolean> {
|
|
421
|
+
if (!Number.isSafeInteger(input.retryAfterMs) || input.retryAfterMs < 0) {
|
|
422
|
+
throw new Error("Connected command reconciliation retry delay is invalid");
|
|
423
|
+
}
|
|
424
|
+
const outcome = input.outcome.trim().slice(0, 64);
|
|
425
|
+
if (!outcome) throw new Error("Connected command reconciliation outcome must not be empty");
|
|
426
|
+
return await withRlsContext(
|
|
427
|
+
db,
|
|
428
|
+
{ accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
|
|
429
|
+
async (scopedDb) => {
|
|
430
|
+
const rows = await scopedDb
|
|
431
|
+
.update(schema.sessionBackgroundCommands)
|
|
432
|
+
.set({
|
|
433
|
+
reconcileAfter: new Date(Date.now() + input.retryAfterMs),
|
|
434
|
+
reconcileClaimId: null,
|
|
435
|
+
reconcileClaimedAt: null,
|
|
436
|
+
lastReconcileOutcome: outcome,
|
|
437
|
+
updatedAt: new Date(),
|
|
438
|
+
})
|
|
439
|
+
.where(connectedClaimIdentityWhere(input.claim))
|
|
440
|
+
.returning({ id: schema.sessionBackgroundCommands.id });
|
|
441
|
+
return rows.length === 1;
|
|
442
|
+
},
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Settle only from the proof already checkpointed under this exact claim. */
|
|
447
|
+
export async function settleClaimedConnectedMachineBackgroundCommand(
|
|
448
|
+
db: Database,
|
|
449
|
+
input: { claim: ConnectedMachineBackgroundCommandClaim },
|
|
450
|
+
): Promise<boolean> {
|
|
451
|
+
return await withRlsContext(
|
|
452
|
+
db,
|
|
453
|
+
{ accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
|
|
454
|
+
async (scopedDb) => {
|
|
455
|
+
const rows = await scopedDb.execute<{ id: string }>(sql`
|
|
456
|
+
update ${schema.sessionBackgroundCommands} command set
|
|
457
|
+
state = command.reconcile_proof_outcome,
|
|
458
|
+
exit_code = case
|
|
459
|
+
when command.reconcile_proof_outcome = 'exited'
|
|
460
|
+
then command.reconcile_proof_exit_code
|
|
461
|
+
else null
|
|
462
|
+
end,
|
|
463
|
+
settlement_reason = command.reconcile_proof_reason,
|
|
464
|
+
settled_at = command.reconcile_proof_observed_at,
|
|
465
|
+
reconcile_claim_id = null,
|
|
466
|
+
reconcile_claimed_at = null,
|
|
467
|
+
last_reconcile_outcome = 'settled_' || command.reconcile_proof_outcome,
|
|
468
|
+
updated_at = clock_timestamp()
|
|
469
|
+
where command.id = ${input.claim.commandId}
|
|
470
|
+
and command.account_id = ${input.claim.accountId}
|
|
471
|
+
and command.workspace_id = ${input.claim.workspaceId}
|
|
472
|
+
and command.session_id = ${input.claim.sessionId}
|
|
473
|
+
and command.provider = 'connected_machine'
|
|
474
|
+
and command.control_workspace_id = ${input.claim.controlWorkspaceId}
|
|
475
|
+
and command.enrollment_id = ${input.claim.enrollmentId}
|
|
476
|
+
and command.connection_instance_id = ${input.claim.connectionInstanceId}
|
|
477
|
+
and command.op_id = ${input.claim.opId}
|
|
478
|
+
and command.reconcile_claim_id = ${input.claim.claimId}
|
|
479
|
+
and command.state in ('running', 'stopping')
|
|
480
|
+
and command.reconcile_proof_outcome in ('exited', 'lost')
|
|
481
|
+
and command.reconcile_proof_observed_at is not null
|
|
482
|
+
returning command.id
|
|
483
|
+
`);
|
|
484
|
+
return rows.length === 1;
|
|
485
|
+
},
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export async function requestSessionBackgroundCommandCancellation(
|
|
490
|
+
db: Database,
|
|
491
|
+
input: {
|
|
492
|
+
accountId: string;
|
|
493
|
+
workspaceId: string;
|
|
494
|
+
sessionId: string;
|
|
495
|
+
commandId: string;
|
|
496
|
+
subjectId: string;
|
|
497
|
+
},
|
|
498
|
+
): Promise<{ command: SessionBackgroundCommand | null; accepted: boolean }> {
|
|
499
|
+
return await withRlsContext(
|
|
500
|
+
db,
|
|
501
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
502
|
+
async (scopedDb) =>
|
|
503
|
+
await scopedDb.transaction(async (tx) => {
|
|
504
|
+
const [current] = await tx
|
|
505
|
+
.select()
|
|
506
|
+
.from(schema.sessionBackgroundCommands)
|
|
507
|
+
.where(
|
|
508
|
+
and(
|
|
509
|
+
eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
510
|
+
eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
|
|
511
|
+
eq(schema.sessionBackgroundCommands.id, input.commandId),
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
.for("update")
|
|
515
|
+
.limit(1);
|
|
516
|
+
if (!current) return { command: null, accepted: false };
|
|
517
|
+
if (current.state !== "running") {
|
|
518
|
+
return { command: mapCommand(current), accepted: false };
|
|
519
|
+
}
|
|
520
|
+
const [updated] = await tx
|
|
521
|
+
.update(schema.sessionBackgroundCommands)
|
|
522
|
+
.set({
|
|
523
|
+
state: "stopping",
|
|
524
|
+
cancelRequestedAt: new Date(),
|
|
525
|
+
cancelRequestedBy: input.subjectId.slice(0, 1024),
|
|
526
|
+
reconcileAfter: new Date(),
|
|
527
|
+
reconcileClaimId: null,
|
|
528
|
+
reconcileClaimedAt: null,
|
|
529
|
+
updatedAt: new Date(),
|
|
530
|
+
})
|
|
531
|
+
.where(eq(schema.sessionBackgroundCommands.id, current.id))
|
|
532
|
+
.returning();
|
|
533
|
+
if (current.retainedProcessId) {
|
|
534
|
+
await tx
|
|
535
|
+
.update(schema.sandboxRetainedProcesses)
|
|
536
|
+
.set({ reconcileAfter: new Date(), lastReconcileOutcome: "cancel_requested" })
|
|
537
|
+
.where(
|
|
538
|
+
and(
|
|
539
|
+
eq(schema.sandboxRetainedProcesses.id, current.retainedProcessId),
|
|
540
|
+
eq(schema.sandboxRetainedProcesses.state, "active"),
|
|
541
|
+
sql`${schema.sandboxRetainedProcesses.reconcileClaimId} is null`,
|
|
542
|
+
),
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
return { command: mapCommand(updated!), accepted: true };
|
|
546
|
+
}),
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Transfer every active command in one or more session subtrees to the
|
|
552
|
+
* provider-neutral stopping state inside the caller's existing control
|
|
553
|
+
* transaction. The workspace-control mutation already excludes concurrent
|
|
554
|
+
* session-tree changes, so the recursive walk and state transition are one
|
|
555
|
+
* atomic Pause/Cancel effect. Steer deliberately never calls this seam.
|
|
556
|
+
*/
|
|
557
|
+
export async function requestSessionBackgroundCommandCancellationsInTransaction(
|
|
558
|
+
db: Database,
|
|
559
|
+
input: {
|
|
560
|
+
workspaceId: string;
|
|
561
|
+
rootSessionIds: string[];
|
|
562
|
+
subjectId: string;
|
|
563
|
+
},
|
|
564
|
+
): Promise<number> {
|
|
565
|
+
const rootSessionIds = [...new Set(input.rootSessionIds)];
|
|
566
|
+
if (rootSessionIds.length === 0) return 0;
|
|
567
|
+
const rows = await db.execute<{ commandCount: number | string }>(sql`
|
|
568
|
+
with recursive roots(id) as (
|
|
569
|
+
values ${sql.join(
|
|
570
|
+
rootSessionIds.map((sessionId) => sql`(${sessionId}::uuid)`),
|
|
571
|
+
sql`, `,
|
|
572
|
+
)}
|
|
573
|
+
), subtree(id, path, cycle) as (
|
|
574
|
+
select session.id, array[session.id]::uuid[], false
|
|
575
|
+
from roots root
|
|
576
|
+
join ${schema.sessions} session
|
|
577
|
+
on session.workspace_id = ${input.workspaceId}
|
|
578
|
+
and session.id = root.id
|
|
579
|
+
union all
|
|
580
|
+
select child.id, parent.path || child.id, child.id = any(parent.path)
|
|
581
|
+
from subtree parent
|
|
582
|
+
join ${schema.sessions} child
|
|
583
|
+
on child.workspace_id = ${input.workspaceId}
|
|
584
|
+
and child.parent_session_id = parent.id
|
|
585
|
+
where not parent.cycle
|
|
586
|
+
), stopped as (
|
|
587
|
+
update ${schema.sessionBackgroundCommands} command set
|
|
588
|
+
state = 'stopping',
|
|
589
|
+
cancel_requested_at = clock_timestamp(),
|
|
590
|
+
cancel_requested_by = left(${input.subjectId}, 1024),
|
|
591
|
+
reconcile_after = clock_timestamp(),
|
|
592
|
+
reconcile_claim_id = null,
|
|
593
|
+
reconcile_claimed_at = null,
|
|
594
|
+
updated_at = clock_timestamp()
|
|
595
|
+
where command.workspace_id = ${input.workspaceId}
|
|
596
|
+
and command.session_id in (select id from subtree where not cycle)
|
|
597
|
+
and command.state = 'running'
|
|
598
|
+
returning command.retained_process_id
|
|
599
|
+
), nudged as (
|
|
600
|
+
update ${schema.sandboxRetainedProcesses} process set
|
|
601
|
+
reconcile_after = clock_timestamp(),
|
|
602
|
+
reconcile_claim_id = null,
|
|
603
|
+
reconcile_claimed_at = null,
|
|
604
|
+
last_reconcile_outcome = 'cancel_requested'
|
|
605
|
+
where process.id in (
|
|
606
|
+
select stopped.retained_process_id
|
|
607
|
+
from stopped
|
|
608
|
+
where stopped.retained_process_id is not null
|
|
609
|
+
)
|
|
610
|
+
and process.state = 'active'
|
|
611
|
+
returning process.id
|
|
612
|
+
)
|
|
613
|
+
select count(*)::integer as "commandCount" from stopped
|
|
614
|
+
`);
|
|
615
|
+
return Number(rows[0]?.commandCount ?? 0);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Workspace Pause owns every active command in that workspace. */
|
|
619
|
+
export async function requestWorkspaceBackgroundCommandCancellationsInTransaction(
|
|
620
|
+
db: Database,
|
|
621
|
+
input: { workspaceId: string; subjectId: string },
|
|
622
|
+
): Promise<number> {
|
|
623
|
+
const rows = await db.execute<{ commandCount: number | string }>(sql`
|
|
624
|
+
with stopped as (
|
|
625
|
+
update ${schema.sessionBackgroundCommands} command set
|
|
626
|
+
state = 'stopping',
|
|
627
|
+
cancel_requested_at = clock_timestamp(),
|
|
628
|
+
cancel_requested_by = left(${input.subjectId}, 1024),
|
|
629
|
+
reconcile_after = clock_timestamp(),
|
|
630
|
+
reconcile_claim_id = null,
|
|
631
|
+
reconcile_claimed_at = null,
|
|
632
|
+
updated_at = clock_timestamp()
|
|
633
|
+
where command.workspace_id = ${input.workspaceId}
|
|
634
|
+
and command.state = 'running'
|
|
635
|
+
returning command.retained_process_id
|
|
636
|
+
), nudged as (
|
|
637
|
+
update ${schema.sandboxRetainedProcesses} process set
|
|
638
|
+
reconcile_after = clock_timestamp(),
|
|
639
|
+
reconcile_claim_id = null,
|
|
640
|
+
reconcile_claimed_at = null,
|
|
641
|
+
last_reconcile_outcome = 'cancel_requested'
|
|
642
|
+
where process.id in (
|
|
643
|
+
select stopped.retained_process_id
|
|
644
|
+
from stopped
|
|
645
|
+
where stopped.retained_process_id is not null
|
|
646
|
+
)
|
|
647
|
+
and process.state = 'active'
|
|
648
|
+
returning process.id
|
|
649
|
+
)
|
|
650
|
+
select count(*)::integer as "commandCount" from stopped
|
|
651
|
+
`);
|
|
652
|
+
return Number(rows[0]?.commandCount ?? 0);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export async function settleSessionBackgroundCommandForRetainedProcess(
|
|
656
|
+
db: Database,
|
|
657
|
+
input: {
|
|
658
|
+
accountId: string;
|
|
659
|
+
workspaceId: string;
|
|
660
|
+
sessionId: string;
|
|
661
|
+
retainedProcessId: string;
|
|
662
|
+
outcome: "exited" | "lost";
|
|
663
|
+
exitCode: number | null;
|
|
664
|
+
reason: string;
|
|
665
|
+
},
|
|
666
|
+
): Promise<SessionBackgroundCommand | null> {
|
|
667
|
+
return await withRlsContext(
|
|
668
|
+
db,
|
|
669
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
670
|
+
async (scopedDb) => {
|
|
671
|
+
const [row] = await scopedDb
|
|
672
|
+
.update(schema.sessionBackgroundCommands)
|
|
673
|
+
.set({
|
|
674
|
+
state: input.outcome,
|
|
675
|
+
exitCode: input.outcome === "exited" ? input.exitCode : null,
|
|
676
|
+
settlementReason: input.reason.slice(0, 512),
|
|
677
|
+
settledAt: new Date(),
|
|
678
|
+
reconcileClaimId: null,
|
|
679
|
+
reconcileClaimedAt: null,
|
|
680
|
+
lastReconcileOutcome: `settled_${input.outcome}`,
|
|
681
|
+
updatedAt: new Date(),
|
|
682
|
+
})
|
|
683
|
+
.where(
|
|
684
|
+
and(
|
|
685
|
+
eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
686
|
+
eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
|
|
687
|
+
eq(schema.sessionBackgroundCommands.retainedProcessId, input.retainedProcessId),
|
|
688
|
+
inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
|
|
689
|
+
),
|
|
690
|
+
)
|
|
691
|
+
.returning();
|
|
692
|
+
return row ? mapCommand(row) : null;
|
|
693
|
+
},
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export async function settleConnectedMachineSessionBackgroundCommand(
|
|
698
|
+
db: Database,
|
|
699
|
+
input: {
|
|
700
|
+
accountId: string;
|
|
701
|
+
workspaceId: string;
|
|
702
|
+
sessionId: string;
|
|
703
|
+
commandId: string;
|
|
704
|
+
controlWorkspaceId: string;
|
|
705
|
+
enrollmentId: string;
|
|
706
|
+
connectionInstanceId: string;
|
|
707
|
+
opId: string;
|
|
708
|
+
outcome: "exited" | "lost";
|
|
709
|
+
exitCode: number | null;
|
|
710
|
+
reason: string;
|
|
711
|
+
},
|
|
712
|
+
): Promise<SessionBackgroundCommand | null> {
|
|
713
|
+
return await withRlsContext(
|
|
714
|
+
db,
|
|
715
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
716
|
+
async (scopedDb) => {
|
|
717
|
+
const settledAt = new Date();
|
|
718
|
+
const reason = input.reason.trim().slice(0, 512);
|
|
719
|
+
if (!reason) throw new Error("Connected command settlement reason must not be empty");
|
|
720
|
+
if (input.outcome === "exited" && input.exitCode === null) {
|
|
721
|
+
throw new Error("Connected command exit settlement requires an exit code");
|
|
722
|
+
}
|
|
723
|
+
if (input.outcome === "lost" && input.exitCode !== null) {
|
|
724
|
+
throw new Error("Connected command loss settlement cannot carry an exit code");
|
|
725
|
+
}
|
|
726
|
+
const [row] = await scopedDb
|
|
727
|
+
.update(schema.sessionBackgroundCommands)
|
|
728
|
+
.set({
|
|
729
|
+
state: input.outcome,
|
|
730
|
+
exitCode: input.outcome === "exited" ? input.exitCode : null,
|
|
731
|
+
settlementReason: reason,
|
|
732
|
+
settledAt,
|
|
733
|
+
reconcileClaimId: null,
|
|
734
|
+
reconcileClaimedAt: null,
|
|
735
|
+
reconcileProofOutcome: input.outcome,
|
|
736
|
+
reconcileProofExitCode: input.outcome === "exited" ? input.exitCode : null,
|
|
737
|
+
reconcileProofReason: reason,
|
|
738
|
+
reconcileProofObservedAt: settledAt,
|
|
739
|
+
lastReconcileOutcome: `settled_${input.outcome}`,
|
|
740
|
+
updatedAt: new Date(),
|
|
741
|
+
})
|
|
742
|
+
.where(
|
|
743
|
+
and(
|
|
744
|
+
eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
745
|
+
eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
|
|
746
|
+
eq(schema.sessionBackgroundCommands.id, input.commandId),
|
|
747
|
+
eq(schema.sessionBackgroundCommands.provider, "connected_machine"),
|
|
748
|
+
eq(schema.sessionBackgroundCommands.controlWorkspaceId, input.controlWorkspaceId),
|
|
749
|
+
eq(schema.sessionBackgroundCommands.enrollmentId, input.enrollmentId),
|
|
750
|
+
eq(schema.sessionBackgroundCommands.connectionInstanceId, input.connectionInstanceId),
|
|
751
|
+
eq(schema.sessionBackgroundCommands.opId, input.opId),
|
|
752
|
+
inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
|
|
753
|
+
),
|
|
754
|
+
)
|
|
755
|
+
.returning();
|
|
756
|
+
return row ? mapCommand(row) : null;
|
|
757
|
+
},
|
|
758
|
+
);
|
|
759
|
+
}
|