@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
package/src/session-control.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
boundWorkspaceControlEvent,
|
|
4
|
+
childPausedClassification,
|
|
4
5
|
McpPersonalConnectionDelegations,
|
|
5
6
|
workspaceControlUtf8Bytes,
|
|
6
7
|
type SessionMcpApprovalPolicy,
|
|
@@ -9,12 +10,25 @@ import {
|
|
|
9
10
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
10
11
|
import type { Database, SessionActivityDatabase } from "./database";
|
|
11
12
|
import { withLosslessContentWriteVersion } from "./lossless-json";
|
|
13
|
+
import { nestedPostgresSqlState } from "./persistence-errors";
|
|
12
14
|
import * as schema from "./schema";
|
|
15
|
+
import {
|
|
16
|
+
boundedChildPausedReason,
|
|
17
|
+
childLifecycleNoticesEnabled,
|
|
18
|
+
childPausedDedupeKey,
|
|
19
|
+
childPausedSummary,
|
|
20
|
+
childRequiresActionResolvedDedupeKey,
|
|
21
|
+
childRequiresActionResolvedSummary,
|
|
22
|
+
} from "./child-lifecycle-notices";
|
|
13
23
|
import { closePendingSessionToolCallsInTransaction } from "./session-tool-call-settlement";
|
|
14
24
|
import {
|
|
15
25
|
mirrorSessionRealtimeContextInTransaction,
|
|
16
26
|
renderRealtimeHumanInputResponseContext,
|
|
17
27
|
} from "./session-realtime-mirror";
|
|
28
|
+
import {
|
|
29
|
+
requestSessionBackgroundCommandCancellationsInTransaction,
|
|
30
|
+
requestWorkspaceBackgroundCommandCancellationsInTransaction,
|
|
31
|
+
} from "./session-background-commands";
|
|
18
32
|
|
|
19
33
|
type SessionEventInsertWithPayload = typeof schema.sessionEvents.$inferInsert & {
|
|
20
34
|
payload: unknown;
|
|
@@ -84,6 +98,10 @@ export type EffectiveSessionControl = {
|
|
|
84
98
|
interruptionPendingCount: number;
|
|
85
99
|
quiescencePendingCount: number;
|
|
86
100
|
} | null;
|
|
101
|
+
backgroundCommandSettlement: {
|
|
102
|
+
state: "stopping";
|
|
103
|
+
commandCount: number;
|
|
104
|
+
} | null;
|
|
87
105
|
};
|
|
88
106
|
|
|
89
107
|
type SettlementAttemptCounts = {
|
|
@@ -300,23 +318,8 @@ export async function assertAgentCommandAuthorityInTransaction(
|
|
|
300
318
|
if (input.action === "steer" && input.targetSessionId === input.actor.sessionId) {
|
|
301
319
|
throw new AgentCommandAuthorityError("SELF_STEER", "An agent cannot steer its own session");
|
|
302
320
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const targetsDirectChild = targetSession.parentSessionId === callerSession.id;
|
|
306
|
-
const verticalTargetAllowed =
|
|
307
|
-
input.action === "goal"
|
|
308
|
-
? targetsSelf
|
|
309
|
-
: input.action === "message"
|
|
310
|
-
? targetsSelf || targetsDirectParent || targetsDirectChild
|
|
311
|
-
: targetsDirectChild;
|
|
312
|
-
if (!verticalTargetAllowed) {
|
|
313
|
-
throw new AgentCommandAuthorityError(
|
|
314
|
-
"TARGET_NOT_VERTICAL",
|
|
315
|
-
input.action === "message"
|
|
316
|
-
? "An agent may message only its own session, direct parent, or direct child"
|
|
317
|
-
: "An agent may control only a direct child session",
|
|
318
|
-
);
|
|
319
|
-
}
|
|
321
|
+
// Peer, parent, child, and unrelated-root targets are allowed here. Slack-private
|
|
322
|
+
// and user_private denies remain in requireSessionAuthorization.
|
|
320
323
|
}
|
|
321
324
|
|
|
322
325
|
function asSafeRevision(value: number | string | null, label: string): number | null {
|
|
@@ -360,25 +363,188 @@ function lockClause(mode: WorkspaceControlLockMode) {
|
|
|
360
363
|
return mode === "update" ? sql.raw("for update") : sql.raw("for share");
|
|
361
364
|
}
|
|
362
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Default bounded control-prefix wait for request-scoped API mutations (Send,
|
|
368
|
+
* Steer, queued Steer, Pause/Resume/Cancel, workspace Pause/Resume, realtime
|
|
369
|
+
* sync). The bound is one budget across every lock step of the admission
|
|
370
|
+
* (advisory lock, row lock, and the savepoint escalation of a paused branch),
|
|
371
|
+
* not a per-statement timeout. A request that cannot enter the workspace
|
|
372
|
+
* control prefix within the budget fails with the typed, retryable
|
|
373
|
+
* `WorkspaceControlBusyError` before any write instead of holding a pooled
|
|
374
|
+
* connection and a snapshot for as long as the HTTP client (or ingress) keeps
|
|
375
|
+
* the socket open. Worker settlement, claims, and event appends never pass a
|
|
376
|
+
* bound: a legitimate wait there must not become a failed settlement.
|
|
377
|
+
* The deployment value is `OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS`, parsed
|
|
378
|
+
* and validated once at boot by `@opengeni/config`
|
|
379
|
+
* (`settings.workspaceControlLockTimeoutMs`) and handed to this module through
|
|
380
|
+
* `configureWorkspaceControlRequestLockTimeoutMs` when the API app is built.
|
|
381
|
+
* This module never reads `process.env` per request.
|
|
382
|
+
*/
|
|
383
|
+
export const WORKSPACE_CONTROL_REQUEST_LOCK_TIMEOUT_MS = 20_000;
|
|
384
|
+
|
|
385
|
+
let configuredRequestLockTimeoutMs = WORKSPACE_CONTROL_REQUEST_LOCK_TIMEOUT_MS;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Install the validated request-scoped budget for this process. The value is
|
|
389
|
+
* process-global and last-writer-wins: the API calls this exactly once from
|
|
390
|
+
* its settings at app composition (`createApp`), and every later
|
|
391
|
+
* `workspaceControlRequestLockTimeoutMs()` read observes that value. Workers
|
|
392
|
+
* never call it because they never pass a bound. A non-positive or
|
|
393
|
+
* non-integer value is a programming error here (the config layer already
|
|
394
|
+
* rejected it at boot).
|
|
395
|
+
*/
|
|
396
|
+
export function configureWorkspaceControlRequestLockTimeoutMs(lockTimeoutMs: number): void {
|
|
397
|
+
if (!Number.isSafeInteger(lockTimeoutMs) || lockTimeoutMs <= 0) {
|
|
398
|
+
throw new Error("Workspace control request lock timeout must be a positive integer (ms)");
|
|
399
|
+
}
|
|
400
|
+
configuredRequestLockTimeoutMs = lockTimeoutMs;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** The request-scoped budget in milliseconds currently configured for this process. */
|
|
404
|
+
export function workspaceControlRequestLockTimeoutMs(): number {
|
|
405
|
+
return configuredRequestLockTimeoutMs;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export class WorkspaceControlBusyError extends Error {
|
|
409
|
+
readonly code = "WORKSPACE_CONTROL_BUSY";
|
|
410
|
+
|
|
411
|
+
constructor(
|
|
412
|
+
readonly workspaceId: string,
|
|
413
|
+
readonly lockTimeoutMs: number,
|
|
414
|
+
) {
|
|
415
|
+
super("The workspace control row is busy; retry shortly");
|
|
416
|
+
this.name = "WorkspaceControlBusyError";
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* One wall-clock budget shared by every lock step of one control-prefix
|
|
422
|
+
* acquisition. Each step sets `lock_timeout` to the remaining budget, so the
|
|
423
|
+
* total wait never exceeds the requested bound even when the advisory lock,
|
|
424
|
+
* the row lock, and a savepoint escalation each wait in turn.
|
|
425
|
+
*/
|
|
426
|
+
export type WorkspaceControlLockBudget = {
|
|
427
|
+
readonly totalMs: number;
|
|
428
|
+
readonly deadlineAt: number;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
export function workspaceControlLockBudget(lockTimeoutMs: number): WorkspaceControlLockBudget {
|
|
432
|
+
if (!Number.isFinite(lockTimeoutMs) || lockTimeoutMs <= 0) {
|
|
433
|
+
throw new Error("Workspace control lockTimeoutMs must be a positive number");
|
|
434
|
+
}
|
|
435
|
+
const totalMs = Math.max(1, Math.floor(lockTimeoutMs));
|
|
436
|
+
return { totalMs, deadlineAt: Date.now() + totalMs };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export type WorkspaceControlLockOptions = {
|
|
440
|
+
/**
|
|
441
|
+
* Bound the whole prefix acquisition in milliseconds. Omit for
|
|
442
|
+
* worker/lifecycle writers. The previous `lock_timeout` is restored once the
|
|
443
|
+
* prefix is held.
|
|
444
|
+
*/
|
|
445
|
+
lockTimeoutMs?: number;
|
|
446
|
+
/** Share an already running budget (used by the admission helper). */
|
|
447
|
+
budget?: WorkspaceControlLockBudget;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
function advisoryLockFunction(mode: Exclude<WorkspaceControlLockMode, "none">) {
|
|
451
|
+
return mode === "update"
|
|
452
|
+
? sql.raw("pg_advisory_xact_lock")
|
|
453
|
+
: sql.raw("pg_advisory_xact_lock_shared");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function lockBudgetFor(options: WorkspaceControlLockOptions): WorkspaceControlLockBudget | null {
|
|
457
|
+
if (options.budget) return options.budget;
|
|
458
|
+
if (options.lockTimeoutMs === undefined) return null;
|
|
459
|
+
return workspaceControlLockBudget(options.lockTimeoutMs);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Run one lock-acquiring statement under the remaining budget. */
|
|
463
|
+
async function boundedLockStep<T>(
|
|
464
|
+
db: Database,
|
|
465
|
+
workspaceId: string,
|
|
466
|
+
budget: WorkspaceControlLockBudget | null,
|
|
467
|
+
step: () => Promise<T>,
|
|
468
|
+
): Promise<T> {
|
|
469
|
+
if (!budget) return await step();
|
|
470
|
+
const remainingMs = budget.deadlineAt - Date.now();
|
|
471
|
+
if (remainingMs <= 0) throw new WorkspaceControlBusyError(workspaceId, budget.totalMs);
|
|
472
|
+
await db.execute(
|
|
473
|
+
sql`select set_config('lock_timeout', ${`${Math.max(1, Math.floor(remainingMs))}ms`}, true)`,
|
|
474
|
+
);
|
|
475
|
+
try {
|
|
476
|
+
return await step();
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (nestedPostgresSqlState(error) === "55P03") {
|
|
479
|
+
// Deliberately no `cause`: the 55P03 must not stay reachable through the
|
|
480
|
+
// cause chain, or persistence-retry classification and the delete-tree
|
|
481
|
+
// `live_sandboxes` 55P03 mapping would reclassify this typed busy error.
|
|
482
|
+
throw new WorkspaceControlBusyError(workspaceId, budget.totalMs);
|
|
483
|
+
}
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Lock the mandatory workspace inference-control row.
|
|
490
|
+
*
|
|
491
|
+
* `share` and `update` first take a transaction-scoped advisory lock on
|
|
492
|
+
* `hashtextextended('workspace-control:<workspace id>', 0)` in the matching
|
|
493
|
+
* mode and only then the row lock. The advisory lock is what makes the prefix
|
|
494
|
+
* fair: PostgreSQL row locks let a new FOR SHARE requester join a share-locked
|
|
495
|
+
* tuple without queueing behind an already waiting FOR UPDATE, so under
|
|
496
|
+
* continuous share traffic (claims, settlement, appends across many sessions)
|
|
497
|
+
* a genuine control mutation can starve indefinitely. Heavyweight locks have a
|
|
498
|
+
* FIFO wait queue: a shared request conflicts with a waiting exclusive request
|
|
499
|
+
* and queues behind it, so a mutator waits only for the holders that preceded
|
|
500
|
+
* it, and sharers keep flowing between mutations. The row lock is retained
|
|
501
|
+
* unchanged for FK/consistency semantics and for SQL-function writers that
|
|
502
|
+
* lock the row directly (session-create triggers and capability functions).
|
|
503
|
+
*
|
|
504
|
+
* Invariant: a transaction that locks this row through any other path (a
|
|
505
|
+
* SECURITY DEFINER function, a trigger, raw SQL) must already hold the prefix
|
|
506
|
+
* through this helper or must never call this helper afterwards for the same
|
|
507
|
+
* workspace. Requesting the advisory lock after holding the row lock can
|
|
508
|
+
* deadlock against a mutator that holds the advisory lock and waits for the
|
|
509
|
+
* row. Every current SQL-function locker either runs in its own transaction or
|
|
510
|
+
* runs after this helper in the same transaction.
|
|
511
|
+
*/
|
|
363
512
|
export async function lockWorkspaceInferenceControl(
|
|
364
513
|
db: Database,
|
|
365
514
|
workspaceId: string,
|
|
366
515
|
mode: WorkspaceControlLockMode,
|
|
516
|
+
options: WorkspaceControlLockOptions = {},
|
|
367
517
|
): Promise<WorkspaceControlRow> {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
518
|
+
const budget = lockBudgetFor(options);
|
|
519
|
+
const [prior] = budget
|
|
520
|
+
? await db.execute<{ value: string }>(sql`select current_setting('lock_timeout') as value`)
|
|
521
|
+
: [];
|
|
522
|
+
if (mode !== "none") {
|
|
523
|
+
await boundedLockStep(db, workspaceId, budget, () =>
|
|
524
|
+
db.execute(
|
|
525
|
+
sql`select ${advisoryLockFunction(mode)}(hashtextextended(${`workspace-control:${workspaceId}`}, 0))`,
|
|
526
|
+
),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const rows = await boundedLockStep(db, workspaceId, budget, () =>
|
|
530
|
+
db.execute<WorkspaceControlRow>(sql`
|
|
531
|
+
select
|
|
532
|
+
workspace_id as "workspaceId",
|
|
533
|
+
account_id as "accountId",
|
|
534
|
+
revision,
|
|
535
|
+
workspace_state as "workspaceState",
|
|
536
|
+
workspace_pause_revision as "workspacePauseRevision",
|
|
537
|
+
reason,
|
|
538
|
+
changed_by as "changedBy",
|
|
539
|
+
changed_at as "changedAt"
|
|
540
|
+
from ${schema.workspaceInferenceControls}
|
|
541
|
+
where workspace_id = ${workspaceId}
|
|
542
|
+
${lockClause(mode)}
|
|
543
|
+
`),
|
|
544
|
+
);
|
|
545
|
+
if (budget) {
|
|
546
|
+
await db.execute(sql`select set_config('lock_timeout', ${prior?.value ?? "0"}, true)`);
|
|
547
|
+
}
|
|
382
548
|
const row = rows[0];
|
|
383
549
|
if (!row) {
|
|
384
550
|
throw new SessionControlInvariantError(
|
|
@@ -388,6 +554,69 @@ export async function lockWorkspaceInferenceControl(
|
|
|
388
554
|
return row;
|
|
389
555
|
}
|
|
390
556
|
|
|
557
|
+
export type WorkspaceControlAdmissionLock = {
|
|
558
|
+
control: WorkspaceControlRow;
|
|
559
|
+
mode: "share" | "update";
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const WORKSPACE_CONTROL_ADMISSION_SAVEPOINT = "opengeni_workspace_control_admission";
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Lock the workspace control prefix for an admission. Callers that may resume
|
|
566
|
+
* a paused branch request escalation; inert queue admission stays shared.
|
|
567
|
+
*
|
|
568
|
+
* The common case (the target branch is active) holds only `FOR SHARE`. That
|
|
569
|
+
* still excludes every control mutation (Pause/Resume/Cancel, workspace
|
|
570
|
+
* control, auto-resume all take `FOR UPDATE`), so the effective state observed
|
|
571
|
+
* here cannot change for the rest of the transaction, while other Sends,
|
|
572
|
+
* Steers, claims, and event appends on the workspace proceed concurrently.
|
|
573
|
+
*
|
|
574
|
+
* When the branch is paused the admission must advance the workspace control
|
|
575
|
+
* revision, so it needs `FOR UPDATE`. Upgrading in place would deadlock as soon
|
|
576
|
+
* as two sharers decide to upgrade together (each waits for the other's share
|
|
577
|
+
* lock), so the shared prefix is taken inside a savepoint and rolled back
|
|
578
|
+
* before the exclusive prefix is requested: PostgreSQL releases row locks and
|
|
579
|
+
* transaction-level advisory locks acquired inside a rolled-back savepoint, so
|
|
580
|
+
* the exclusive request queues normally behind other holders.
|
|
581
|
+
*
|
|
582
|
+
* Callers that already hold the prefix exclusively (for example a realtime
|
|
583
|
+
* sync that admitted a delegation) simply observe their own stronger lock.
|
|
584
|
+
*/
|
|
585
|
+
export async function lockWorkspaceInferenceControlForAdmission(
|
|
586
|
+
db: Database,
|
|
587
|
+
input: {
|
|
588
|
+
workspaceId: string;
|
|
589
|
+
sessionId: string;
|
|
590
|
+
resumePausedBranch: boolean;
|
|
591
|
+
} & WorkspaceControlLockOptions,
|
|
592
|
+
): Promise<WorkspaceControlAdmissionLock> {
|
|
593
|
+
// One budget spans the shared attempt and any escalation to the exclusive
|
|
594
|
+
// prefix, so the caller's bound is the total wait, not a per-step timeout.
|
|
595
|
+
const lockOptions: WorkspaceControlLockOptions =
|
|
596
|
+
input.lockTimeoutMs === undefined
|
|
597
|
+
? {}
|
|
598
|
+
: { budget: workspaceControlLockBudget(input.lockTimeoutMs) };
|
|
599
|
+
await db.execute(sql.raw(`savepoint ${WORKSPACE_CONTROL_ADMISSION_SAVEPOINT}`));
|
|
600
|
+
const shared = await lockWorkspaceInferenceControl(db, input.workspaceId, "share", lockOptions);
|
|
601
|
+
if (!input.resumePausedBranch) {
|
|
602
|
+
await db.execute(sql.raw(`release savepoint ${WORKSPACE_CONTROL_ADMISSION_SAVEPOINT}`));
|
|
603
|
+
return { control: shared, mode: "share" };
|
|
604
|
+
}
|
|
605
|
+
const effective = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
606
|
+
workspaceControl: shared,
|
|
607
|
+
});
|
|
608
|
+
if (effective.state === "active") {
|
|
609
|
+
await db.execute(sql.raw(`release savepoint ${WORKSPACE_CONTROL_ADMISSION_SAVEPOINT}`));
|
|
610
|
+
return { control: shared, mode: "share" };
|
|
611
|
+
}
|
|
612
|
+
await db.execute(sql.raw(`rollback to savepoint ${WORKSPACE_CONTROL_ADMISSION_SAVEPOINT}`));
|
|
613
|
+
await db.execute(sql.raw(`release savepoint ${WORKSPACE_CONTROL_ADMISSION_SAVEPOINT}`));
|
|
614
|
+
return {
|
|
615
|
+
control: await lockWorkspaceInferenceControl(db, input.workspaceId, "update", lockOptions),
|
|
616
|
+
mode: "update",
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
391
620
|
export type SessionEventWriteLockInput = {
|
|
392
621
|
workspaceId: string;
|
|
393
622
|
/**
|
|
@@ -414,12 +643,19 @@ export type SessionEventWriteLocks = {
|
|
|
414
643
|
/**
|
|
415
644
|
* Establish the one canonical lock prefix for every `session_events` writer:
|
|
416
645
|
*
|
|
417
|
-
* workspace_inference_controls
|
|
646
|
+
* workspace-control advisory lock + workspace_inference_controls row
|
|
647
|
+
* (when control-aware; see `lockWorkspaceInferenceControl`)
|
|
418
648
|
* -> actual workspaces row FOR KEY SHARE
|
|
419
649
|
* -> session rows FOR NO KEY UPDATE, UUID ordered
|
|
420
650
|
* -> exact turn rows FOR UPDATE, UUID ordered
|
|
421
651
|
* -> exact attempt rows FOR UPDATE, UUID ordered
|
|
422
652
|
*
|
|
653
|
+
* Control-aware writers that do not mutate the control row take it `share`;
|
|
654
|
+
* only genuine control mutations (Pause/Resume/Cancel, workspace control,
|
|
655
|
+
* auto-resume, settings narrowing, quiescent tree deletion) take `update`.
|
|
656
|
+
* Prompt admission declares whether it may resume a paused branch through
|
|
657
|
+
* `lockWorkspaceInferenceControlForAdmission`.
|
|
658
|
+
*
|
|
423
659
|
* `FOR KEY SHARE` is deliberate. Event inserts need the workspace key to remain
|
|
424
660
|
* stable for their FK, but they do not mutate the workspace. The old generic
|
|
425
661
|
* `FOR UPDATE` lock serialized unrelated sessions and inverted the activity
|
|
@@ -434,6 +670,11 @@ export type SessionEventWriteLocks = {
|
|
|
434
670
|
* this helper and may discover exact turn IDs only after locking the session.
|
|
435
671
|
* They use the explicit `already_locked` stages, while retaining the same
|
|
436
672
|
* monotonic table order. New event writers should prefer one complete call.
|
|
673
|
+
*
|
|
674
|
+
* Retained-screenshot prepare is not an event writer, but its INSERT still
|
|
675
|
+
* KEY SHAREs the turn and attempt through FKs. It must take this same prefix
|
|
676
|
+
* first: PostgreSQL fires those FK checks in catalog name/OID order, which can
|
|
677
|
+
* be attempt-then-turn and otherwise deadlocks against this helper.
|
|
437
678
|
*/
|
|
438
679
|
export async function lockSessionEventWriteRows(
|
|
439
680
|
db: Database,
|
|
@@ -523,6 +764,7 @@ export async function registerSessionTurnAttemptClaim(
|
|
|
523
764
|
authorityEpoch: number;
|
|
524
765
|
authorityVisibility: "user_private" | "workspace_shared";
|
|
525
766
|
authorityOwnerOrganizationMembershipId: string | null;
|
|
767
|
+
personalResourceProtocolVersion: number;
|
|
526
768
|
mcpApprovalPolicies: Record<string, SessionMcpApprovalPolicy>;
|
|
527
769
|
connectorActionPolicies: schema.ConnectorActionPolicySnapshotEntry[];
|
|
528
770
|
},
|
|
@@ -577,6 +819,7 @@ export async function registerSessionTurnAttemptClaim(
|
|
|
577
819
|
existing.temporalWorkflowId !== input.temporalWorkflowId ||
|
|
578
820
|
existing.temporalWorkflowRunId !== input.temporalWorkflowRunId ||
|
|
579
821
|
existing.temporalActivityId !== input.temporalActivityId ||
|
|
822
|
+
existing.personalResourceProtocolVersion !== input.personalResourceProtocolVersion ||
|
|
580
823
|
JSON.stringify(existing.connectorActionPolicies) !==
|
|
581
824
|
JSON.stringify(input.connectorActionPolicies) ||
|
|
582
825
|
existing.state === "closed"
|
|
@@ -912,6 +1155,7 @@ function projectEffectiveControl(
|
|
|
912
1155
|
targetId: string,
|
|
913
1156
|
rows: AncestryRow[],
|
|
914
1157
|
settlementAttempts: SettlementAttemptCounts,
|
|
1158
|
+
stoppingBackgroundCommandCount: number,
|
|
915
1159
|
): EffectiveSessionControl {
|
|
916
1160
|
assertCompleteAncestry(targetId, rows);
|
|
917
1161
|
const workspaceRevision = asSafeRevision(workspace.revision, "workspace control revision")!;
|
|
@@ -1045,9 +1289,49 @@ function projectEffectiveControl(
|
|
|
1045
1289
|
},
|
|
1046
1290
|
settlement:
|
|
1047
1291
|
settlementAttempts.attemptCount > 0 ? { state: "stopping", ...settlementAttempts } : null,
|
|
1292
|
+
backgroundCommandSettlement:
|
|
1293
|
+
stoppingBackgroundCommandCount > 0
|
|
1294
|
+
? { state: "stopping", commandCount: stoppingBackgroundCommandCount }
|
|
1295
|
+
: null,
|
|
1048
1296
|
};
|
|
1049
1297
|
}
|
|
1050
1298
|
|
|
1299
|
+
async function stoppingBackgroundCommandCounts(
|
|
1300
|
+
db: Database,
|
|
1301
|
+
workspaceId: string,
|
|
1302
|
+
sessionIds: string[],
|
|
1303
|
+
): Promise<Map<string, number>> {
|
|
1304
|
+
const rows = await db.execute<{ sessionId: string; commandCount: number | string }>(sql`
|
|
1305
|
+
with recursive targets(id) as (values ${targetValues(sessionIds)}),
|
|
1306
|
+
command_ancestry(command_id, ancestor_id, depth, path) as (
|
|
1307
|
+
select command.id, command.session_id, 0::integer, array[command.session_id]::uuid[]
|
|
1308
|
+
from ${schema.sessionBackgroundCommands} command
|
|
1309
|
+
where command.workspace_id = ${workspaceId}
|
|
1310
|
+
and command.state = 'stopping'
|
|
1311
|
+
union all
|
|
1312
|
+
select ancestry.command_id, current.parent_session_id,
|
|
1313
|
+
ancestry.depth + 1, ancestry.path || current.parent_session_id
|
|
1314
|
+
from command_ancestry ancestry
|
|
1315
|
+
join ${schema.sessions} current
|
|
1316
|
+
on current.workspace_id = ${workspaceId}
|
|
1317
|
+
and current.id = ancestry.ancestor_id
|
|
1318
|
+
where current.parent_session_id is not null
|
|
1319
|
+
and not current.parent_session_id = any(ancestry.path)
|
|
1320
|
+
and ancestry.depth < ${SESSION_ANCESTRY_LIMIT}
|
|
1321
|
+
)
|
|
1322
|
+
select target.id as "sessionId", count(distinct ancestry.command_id)::integer as "commandCount"
|
|
1323
|
+
from targets target
|
|
1324
|
+
join command_ancestry ancestry on ancestry.ancestor_id = target.id
|
|
1325
|
+
group by target.id
|
|
1326
|
+
`);
|
|
1327
|
+
return new Map(
|
|
1328
|
+
rows.map((row: { sessionId: string; commandCount: number | string }) => [
|
|
1329
|
+
row.sessionId,
|
|
1330
|
+
Number(row.commandCount),
|
|
1331
|
+
]),
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1051
1335
|
async function settlementAttemptCounts(
|
|
1052
1336
|
db: Database,
|
|
1053
1337
|
workspaceId: string,
|
|
@@ -1170,6 +1454,7 @@ export async function evaluateSessionControls(
|
|
|
1170
1454
|
options.workspaceControl ??
|
|
1171
1455
|
(await lockWorkspaceInferenceControl(db, workspaceId, options.lock ?? "share"));
|
|
1172
1456
|
const stopping = await settlementAttemptCounts(db, workspaceId, uniqueIds);
|
|
1457
|
+
const stoppingCommands = await stoppingBackgroundCommandCounts(db, workspaceId, uniqueIds);
|
|
1173
1458
|
const result = new Map<string, EffectiveSessionControl>();
|
|
1174
1459
|
if (uniqueIds.length <= TARGET_PATH_PROJECTION_LIMIT) {
|
|
1175
1460
|
// PostgreSQL's direct target-path plan is substantially faster for the
|
|
@@ -1189,6 +1474,7 @@ export async function evaluateSessionControls(
|
|
|
1189
1474
|
sessionId,
|
|
1190
1475
|
ancestryByTarget.get(sessionId) ?? [],
|
|
1191
1476
|
stopping.get(sessionId) ?? NO_SETTLEMENT_ATTEMPTS,
|
|
1477
|
+
stoppingCommands.get(sessionId) ?? 0,
|
|
1192
1478
|
),
|
|
1193
1479
|
);
|
|
1194
1480
|
}
|
|
@@ -1207,6 +1493,7 @@ export async function evaluateSessionControls(
|
|
|
1207
1493
|
sessionId,
|
|
1208
1494
|
ancestryRowsForTarget(sessionId, ancestry),
|
|
1209
1495
|
stopping.get(sessionId) ?? NO_SETTLEMENT_ATTEMPTS,
|
|
1496
|
+
stoppingCommands.get(sessionId) ?? 0,
|
|
1210
1497
|
),
|
|
1211
1498
|
);
|
|
1212
1499
|
}
|
|
@@ -1500,7 +1787,7 @@ export async function reserveSessionCommandReceipt(
|
|
|
1500
1787
|
if (
|
|
1501
1788
|
identityScope === "goal_operation" &&
|
|
1502
1789
|
(input.actor.type !== "agent_attempt" ||
|
|
1503
|
-
!["goal.update", "goal.progress"].includes(input.action) ||
|
|
1790
|
+
!["goal.update", "goal.progress", "goal.wait"].includes(input.action) ||
|
|
1504
1791
|
input.targetSessionId === null ||
|
|
1505
1792
|
input.targetTurnId !== null)
|
|
1506
1793
|
) {
|
|
@@ -1940,13 +2227,57 @@ export type SessionControlMutationResult = {
|
|
|
1940
2227
|
sessionControlEventId: string;
|
|
1941
2228
|
workspaceControlEventId: string;
|
|
1942
2229
|
interruptionCount: number;
|
|
2230
|
+
backgroundCommandCount: number;
|
|
1943
2231
|
wakeCount: number;
|
|
1944
2232
|
cancelledSessionCount: number;
|
|
1945
2233
|
cancelledTurnCount: number;
|
|
1946
2234
|
affectedSessionEvents: Array<{ sessionId: string; eventIds: string[] }>;
|
|
2235
|
+
workflowWake: SessionControlWorkflowWake | null;
|
|
1947
2236
|
replay: boolean;
|
|
1948
2237
|
};
|
|
1949
2238
|
|
|
2239
|
+
export type SessionControlWorkflowWake = {
|
|
2240
|
+
accountId: string;
|
|
2241
|
+
workspaceId: string;
|
|
2242
|
+
sessionId: string;
|
|
2243
|
+
temporalWorkflowId: string;
|
|
2244
|
+
wakeRevision: number;
|
|
2245
|
+
};
|
|
2246
|
+
|
|
2247
|
+
async function pendingSessionControlWorkflowWake(
|
|
2248
|
+
db: Database,
|
|
2249
|
+
workspaceId: string,
|
|
2250
|
+
sessionId: string,
|
|
2251
|
+
wakeCount: number,
|
|
2252
|
+
): Promise<SessionControlWorkflowWake | null> {
|
|
2253
|
+
if (wakeCount === 0) return null;
|
|
2254
|
+
const [wake] = await db
|
|
2255
|
+
.select({
|
|
2256
|
+
accountId: schema.sessionWorkflowWakeOutbox.accountId,
|
|
2257
|
+
workspaceId: schema.sessionWorkflowWakeOutbox.workspaceId,
|
|
2258
|
+
sessionId: schema.sessionWorkflowWakeOutbox.sessionId,
|
|
2259
|
+
temporalWorkflowId: schema.sessionWorkflowWakeOutbox.temporalWorkflowId,
|
|
2260
|
+
wakeRevision: schema.sessionWorkflowWakeOutbox.wakeRevision,
|
|
2261
|
+
deliveredRevision: schema.sessionWorkflowWakeOutbox.deliveredRevision,
|
|
2262
|
+
})
|
|
2263
|
+
.from(schema.sessionWorkflowWakeOutbox)
|
|
2264
|
+
.where(
|
|
2265
|
+
and(
|
|
2266
|
+
eq(schema.sessionWorkflowWakeOutbox.workspaceId, workspaceId),
|
|
2267
|
+
eq(schema.sessionWorkflowWakeOutbox.sessionId, sessionId),
|
|
2268
|
+
),
|
|
2269
|
+
)
|
|
2270
|
+
.limit(1);
|
|
2271
|
+
if (!wake || wake.wakeRevision <= wake.deliveredRevision) return null;
|
|
2272
|
+
return {
|
|
2273
|
+
accountId: wake.accountId,
|
|
2274
|
+
workspaceId: wake.workspaceId,
|
|
2275
|
+
sessionId: wake.sessionId,
|
|
2276
|
+
temporalWorkflowId: wake.temporalWorkflowId,
|
|
2277
|
+
wakeRevision: wake.wakeRevision,
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
|
|
1950
2281
|
async function loadSessionSubtreeIds(
|
|
1951
2282
|
db: Database,
|
|
1952
2283
|
workspaceId: string,
|
|
@@ -2013,25 +2344,55 @@ async function loadCancellationTurnIds(
|
|
|
2013
2344
|
return rows.map((row) => row.id);
|
|
2014
2345
|
}
|
|
2015
2346
|
|
|
2016
|
-
async function
|
|
2347
|
+
async function loadParentSessionId(
|
|
2348
|
+
db: Database,
|
|
2349
|
+
workspaceId: string,
|
|
2350
|
+
sessionId: string,
|
|
2351
|
+
): Promise<string | null> {
|
|
2352
|
+
const [row] = await db
|
|
2353
|
+
.select({ parentSessionId: schema.sessions.parentSessionId })
|
|
2354
|
+
.from(schema.sessions)
|
|
2355
|
+
.where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId)))
|
|
2356
|
+
.limit(1);
|
|
2357
|
+
return row?.parentSessionId ?? null;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
/**
|
|
2361
|
+
* The one control-plane child -> parent outbox insert (terminal cancellation,
|
|
2362
|
+
* direct Pause, and cancelled human-input resolutions). The caller holds the
|
|
2363
|
+
* canonical prefix with the child and its parent locked UUID-ordered; the
|
|
2364
|
+
* causal parent-turn personal delegation snapshot is copied verbatim.
|
|
2365
|
+
*/
|
|
2366
|
+
async function insertChildOutboxRowInTransaction(
|
|
2017
2367
|
db: Database,
|
|
2018
2368
|
input: {
|
|
2019
2369
|
workspaceId: string;
|
|
2020
|
-
|
|
2370
|
+
childSession: typeof schema.sessions.$inferSelect;
|
|
2021
2371
|
parentSession: typeof schema.sessions.$inferSelect | null;
|
|
2372
|
+
dedupeKey: string;
|
|
2373
|
+
kind: "child_terminal_result" | "child_paused" | "child_requires_action_resolved";
|
|
2374
|
+
classification: "info" | "action_required";
|
|
2375
|
+
summary: string;
|
|
2376
|
+
payload: Record<string, unknown> & { type: string };
|
|
2377
|
+
lineage?: Record<string, unknown>;
|
|
2022
2378
|
},
|
|
2023
2379
|
): Promise<void> {
|
|
2024
|
-
const parentSessionId = input.
|
|
2380
|
+
const parentSessionId = input.childSession.parentSessionId;
|
|
2025
2381
|
if (!parentSessionId) return;
|
|
2026
2382
|
if (!input.parentSession || input.parentSession.id !== parentSessionId) {
|
|
2027
2383
|
throw new SessionControlInvariantError(
|
|
2028
|
-
`Parent session ${parentSessionId} was not locked with
|
|
2384
|
+
`Parent session ${parentSessionId} was not locked with child ${input.childSession.id}`,
|
|
2029
2385
|
);
|
|
2030
2386
|
}
|
|
2031
2387
|
if (input.parentSession.status === "cancelled") return;
|
|
2388
|
+
if (input.payload.type !== input.kind) {
|
|
2389
|
+
throw new SessionControlInvariantError(
|
|
2390
|
+
`Child outbox ${input.kind} payload discriminator mismatch`,
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2032
2393
|
let personalConnectionDelegations: (typeof schema.sessionTurns.$inferSelect)["personalConnectionDelegations"] =
|
|
2033
2394
|
[];
|
|
2034
|
-
if (input.
|
|
2395
|
+
if (input.childSession.parentTurnId) {
|
|
2035
2396
|
const [parentTurn] = await db
|
|
2036
2397
|
.select({
|
|
2037
2398
|
delegations: schema.sessionTurns.personalConnectionDelegations,
|
|
@@ -2041,7 +2402,7 @@ async function enqueueCancelledChildOutboxInTransaction(
|
|
|
2041
2402
|
and(
|
|
2042
2403
|
eq(schema.sessionTurns.workspaceId, input.workspaceId),
|
|
2043
2404
|
eq(schema.sessionTurns.sessionId, parentSessionId),
|
|
2044
|
-
eq(schema.sessionTurns.id, input.
|
|
2405
|
+
eq(schema.sessionTurns.id, input.childSession.parentTurnId),
|
|
2045
2406
|
),
|
|
2046
2407
|
)
|
|
2047
2408
|
.limit(1);
|
|
@@ -2049,7 +2410,7 @@ async function enqueueCancelledChildOutboxInTransaction(
|
|
|
2049
2410
|
const parsed = McpPersonalConnectionDelegations.safeParse(parentTurn.delegations);
|
|
2050
2411
|
if (!parsed.success) {
|
|
2051
2412
|
throw new SessionControlInvariantError(
|
|
2052
|
-
`Invalid personal MCP delegation snapshot at session_turns:${input.workspaceId}:${parentSessionId}:${input.
|
|
2413
|
+
`Invalid personal MCP delegation snapshot at session_turns:${input.workspaceId}:${parentSessionId}:${input.childSession.parentTurnId}`,
|
|
2053
2414
|
);
|
|
2054
2415
|
}
|
|
2055
2416
|
personalConnectionDelegations = parsed.data.map((delegation) => ({
|
|
@@ -2063,26 +2424,23 @@ async function enqueueCancelledChildOutboxInTransaction(
|
|
|
2063
2424
|
withLosslessContentWriteVersion(
|
|
2064
2425
|
withLosslessContentWriteVersion(
|
|
2065
2426
|
{
|
|
2066
|
-
accountId: input.
|
|
2427
|
+
accountId: input.childSession.accountId,
|
|
2067
2428
|
workspaceId: input.workspaceId,
|
|
2068
|
-
sourceSessionId: input.
|
|
2429
|
+
sourceSessionId: input.childSession.id,
|
|
2069
2430
|
targetSessionId: parentSessionId,
|
|
2070
|
-
dedupeKey:
|
|
2071
|
-
kind:
|
|
2072
|
-
classification:
|
|
2073
|
-
sourceId: input.
|
|
2074
|
-
summary:
|
|
2075
|
-
payload:
|
|
2076
|
-
type: "child_terminal_result",
|
|
2077
|
-
childSessionId: input.rootSession.id,
|
|
2078
|
-
status: "cancelled",
|
|
2079
|
-
},
|
|
2431
|
+
dedupeKey: input.dedupeKey,
|
|
2432
|
+
kind: input.kind,
|
|
2433
|
+
classification: input.classification,
|
|
2434
|
+
sourceId: input.childSession.id,
|
|
2435
|
+
summary: input.summary,
|
|
2436
|
+
payload: input.payload,
|
|
2080
2437
|
lineage: {
|
|
2081
|
-
childSessionId: input.
|
|
2438
|
+
childSessionId: input.childSession.id,
|
|
2082
2439
|
parentSessionId,
|
|
2083
|
-
...(input.
|
|
2084
|
-
? { parentTurnId: input.
|
|
2440
|
+
...(input.childSession.parentTurnId
|
|
2441
|
+
? { parentTurnId: input.childSession.parentTurnId }
|
|
2085
2442
|
: {}),
|
|
2443
|
+
...(input.lineage ?? {}),
|
|
2086
2444
|
},
|
|
2087
2445
|
personalConnectionDelegations,
|
|
2088
2446
|
},
|
|
@@ -2101,6 +2459,118 @@ async function enqueueCancelledChildOutboxInTransaction(
|
|
|
2101
2459
|
});
|
|
2102
2460
|
}
|
|
2103
2461
|
|
|
2462
|
+
async function enqueueCancelledChildOutboxInTransaction(
|
|
2463
|
+
db: Database,
|
|
2464
|
+
input: {
|
|
2465
|
+
workspaceId: string;
|
|
2466
|
+
rootSession: typeof schema.sessions.$inferSelect;
|
|
2467
|
+
parentSession: typeof schema.sessions.$inferSelect | null;
|
|
2468
|
+
},
|
|
2469
|
+
): Promise<void> {
|
|
2470
|
+
await insertChildOutboxRowInTransaction(db, {
|
|
2471
|
+
workspaceId: input.workspaceId,
|
|
2472
|
+
childSession: input.rootSession,
|
|
2473
|
+
parentSession: input.parentSession,
|
|
2474
|
+
dedupeKey: `child-completion:${input.rootSession.id}:cancelled`,
|
|
2475
|
+
kind: "child_terminal_result",
|
|
2476
|
+
classification: "info",
|
|
2477
|
+
summary: "Child session was terminally cancelled.",
|
|
2478
|
+
payload: {
|
|
2479
|
+
type: "child_terminal_result",
|
|
2480
|
+
childSessionId: input.rootSession.id,
|
|
2481
|
+
status: "cancelled",
|
|
2482
|
+
},
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/** child_paused notice for a direct Pause of a child; gated by the rollout flag. */
|
|
2487
|
+
async function enqueueChildPausedOutboxInTransaction(
|
|
2488
|
+
db: Database,
|
|
2489
|
+
input: {
|
|
2490
|
+
workspaceId: string;
|
|
2491
|
+
childSession: typeof schema.sessions.$inferSelect;
|
|
2492
|
+
parentSession: typeof schema.sessions.$inferSelect;
|
|
2493
|
+
operationId: string;
|
|
2494
|
+
actorKind: "human" | "api" | "agent";
|
|
2495
|
+
reason: string | null;
|
|
2496
|
+
},
|
|
2497
|
+
): Promise<void> {
|
|
2498
|
+
if (!childLifecycleNoticesEnabled()) return;
|
|
2499
|
+
const payload = {
|
|
2500
|
+
type: "child_paused" as const,
|
|
2501
|
+
childSessionId: input.childSession.id,
|
|
2502
|
+
operationId: input.operationId,
|
|
2503
|
+
actorKind: input.actorKind,
|
|
2504
|
+
reason: boundedChildPausedReason(input.reason),
|
|
2505
|
+
};
|
|
2506
|
+
await insertChildOutboxRowInTransaction(db, {
|
|
2507
|
+
workspaceId: input.workspaceId,
|
|
2508
|
+
childSession: input.childSession,
|
|
2509
|
+
parentSession: input.parentSession,
|
|
2510
|
+
dedupeKey: childPausedDedupeKey({
|
|
2511
|
+
childSessionId: input.childSession.id,
|
|
2512
|
+
receiptId: input.operationId,
|
|
2513
|
+
}),
|
|
2514
|
+
kind: "child_paused",
|
|
2515
|
+
classification: childPausedClassification(input.actorKind),
|
|
2516
|
+
summary: childPausedSummary(input.childSession.id, payload),
|
|
2517
|
+
payload,
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
/**
|
|
2522
|
+
* Terminal cancellation of the selected root's pending human-input rows: each
|
|
2523
|
+
* becomes one child_requires_action_resolved notice for the surviving parent
|
|
2524
|
+
* so its pending child_requires_action can be superseded. Cancelled
|
|
2525
|
+
* descendants do not notify parents inside the same terminal subtree.
|
|
2526
|
+
*/
|
|
2527
|
+
async function enqueueCancelledChildRequiresActionResolvedOutboxInTransaction(
|
|
2528
|
+
db: Database,
|
|
2529
|
+
input: {
|
|
2530
|
+
workspaceId: string;
|
|
2531
|
+
rootSession: typeof schema.sessions.$inferSelect;
|
|
2532
|
+
parentSession: typeof schema.sessions.$inferSelect | null;
|
|
2533
|
+
cancelledHumanInputs: ReadonlyArray<{
|
|
2534
|
+
id: string;
|
|
2535
|
+
sessionId: string;
|
|
2536
|
+
turnId: string;
|
|
2537
|
+
turnGeneration: number;
|
|
2538
|
+
}>;
|
|
2539
|
+
},
|
|
2540
|
+
): Promise<void> {
|
|
2541
|
+
if (!childLifecycleNoticesEnabled() || !input.parentSession) return;
|
|
2542
|
+
for (const humanInput of input.cancelledHumanInputs) {
|
|
2543
|
+
if (humanInput.sessionId !== input.rootSession.id) continue;
|
|
2544
|
+
const payload = {
|
|
2545
|
+
type: "child_requires_action_resolved" as const,
|
|
2546
|
+
childSessionId: input.rootSession.id,
|
|
2547
|
+
childTurnId: humanInput.turnId,
|
|
2548
|
+
childTurnGeneration: humanInput.turnGeneration,
|
|
2549
|
+
requestId: humanInput.id,
|
|
2550
|
+
approvalId: null,
|
|
2551
|
+
outcome: "cancelled" as const,
|
|
2552
|
+
respondedByKind: "system" as const,
|
|
2553
|
+
};
|
|
2554
|
+
await insertChildOutboxRowInTransaction(db, {
|
|
2555
|
+
workspaceId: input.workspaceId,
|
|
2556
|
+
childSession: input.rootSession,
|
|
2557
|
+
parentSession: input.parentSession,
|
|
2558
|
+
dedupeKey: childRequiresActionResolvedDedupeKey({
|
|
2559
|
+
childSessionId: input.rootSession.id,
|
|
2560
|
+
turnId: humanInput.turnId,
|
|
2561
|
+
turnGeneration: humanInput.turnGeneration,
|
|
2562
|
+
requestId: humanInput.id,
|
|
2563
|
+
approvalId: null,
|
|
2564
|
+
}),
|
|
2565
|
+
kind: "child_requires_action_resolved",
|
|
2566
|
+
classification: "info",
|
|
2567
|
+
summary: childRequiresActionResolvedSummary(input.rootSession.id, payload),
|
|
2568
|
+
payload,
|
|
2569
|
+
lineage: { turnId: humanInput.turnId },
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2104
2574
|
async function assertSessionBranchIsNotCancelled(
|
|
2105
2575
|
db: Database,
|
|
2106
2576
|
workspaceId: string,
|
|
@@ -2212,6 +2682,7 @@ async function cancelSessionSubtreeInTransaction(
|
|
|
2212
2682
|
id: string;
|
|
2213
2683
|
sessionId: string;
|
|
2214
2684
|
turnId: string;
|
|
2685
|
+
turnGeneration: number;
|
|
2215
2686
|
questions: (typeof schema.sessionHumanInputRequests.$inferSelect)["questions"];
|
|
2216
2687
|
}> = [];
|
|
2217
2688
|
if (immediatelyCancelledTurnIds.length > 0) {
|
|
@@ -2247,6 +2718,7 @@ async function cancelSessionSubtreeInTransaction(
|
|
|
2247
2718
|
id: schema.sessionHumanInputRequests.id,
|
|
2248
2719
|
sessionId: schema.sessionHumanInputRequests.sessionId,
|
|
2249
2720
|
turnId: schema.sessionHumanInputRequests.turnId,
|
|
2721
|
+
turnGeneration: schema.sessionHumanInputRequests.turnGeneration,
|
|
2250
2722
|
questions: schema.sessionHumanInputRequests.questions,
|
|
2251
2723
|
});
|
|
2252
2724
|
await db
|
|
@@ -2450,6 +2922,12 @@ async function cancelSessionSubtreeInTransaction(
|
|
|
2450
2922
|
if (session.id === input.rootSessionId) eventIds.unshift(input.rootControlEventId);
|
|
2451
2923
|
if (eventIds.length > 0) affectedSessionEvents.push({ sessionId: session.id, eventIds });
|
|
2452
2924
|
}
|
|
2925
|
+
await enqueueCancelledChildRequiresActionResolvedOutboxInTransaction(db, {
|
|
2926
|
+
workspaceId: input.workspaceId,
|
|
2927
|
+
rootSession,
|
|
2928
|
+
parentSession: rootParentSession,
|
|
2929
|
+
cancelledHumanInputs,
|
|
2930
|
+
});
|
|
2453
2931
|
await enqueueCancelledChildOutboxInTransaction(db, {
|
|
2454
2932
|
workspaceId: input.workspaceId,
|
|
2455
2933
|
rootSession,
|
|
@@ -2473,9 +2951,15 @@ export async function mutateSessionControlInTransaction(
|
|
|
2473
2951
|
action: "pause" | "resume" | "cancel";
|
|
2474
2952
|
reason?: string | null;
|
|
2475
2953
|
expectedControlEtag?: string | null;
|
|
2954
|
+
/** Request-scoped callers bound the control prefix wait; lifecycle callers omit it. */
|
|
2955
|
+
controlLockTimeoutMs?: number;
|
|
2476
2956
|
},
|
|
2477
2957
|
): Promise<SessionControlMutationResult> {
|
|
2478
|
-
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update"
|
|
2958
|
+
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update", {
|
|
2959
|
+
...(input.controlLockTimeoutMs !== undefined
|
|
2960
|
+
? { lockTimeoutMs: input.controlLockTimeoutMs }
|
|
2961
|
+
: {}),
|
|
2962
|
+
});
|
|
2479
2963
|
const cancellationSubtree =
|
|
2480
2964
|
input.action === "cancel"
|
|
2481
2965
|
? await loadSessionSubtreeIds(db, input.workspaceId, input.sessionId)
|
|
@@ -2485,6 +2969,14 @@ export async function mutateSessionControlInTransaction(
|
|
|
2485
2969
|
input.action === "cancel"
|
|
2486
2970
|
? await loadCancellationTurnIds(db, input.workspaceId, cancellationSessionIds)
|
|
2487
2971
|
: [];
|
|
2972
|
+
// A direct Pause of a child notifies its parent (child_paused) through the
|
|
2973
|
+
// outbox in this same commit, so the parent joins the UUID-ordered session
|
|
2974
|
+
// lock exactly like Cancel locks the root's surviving parent.
|
|
2975
|
+
const pauseParentSessionId =
|
|
2976
|
+
input.action === "pause" && childLifecycleNoticesEnabled()
|
|
2977
|
+
? await loadParentSessionId(db, input.workspaceId, input.sessionId)
|
|
2978
|
+
: null;
|
|
2979
|
+
const lineageParentSessionId = cancellationSubtree.rootParentSessionId ?? pauseParentSessionId;
|
|
2488
2980
|
const locks = await lockSessionEventWriteRows(db, {
|
|
2489
2981
|
workspaceId: input.workspaceId,
|
|
2490
2982
|
controlLock: "already_locked",
|
|
@@ -2493,27 +2985,20 @@ export async function mutateSessionControlInTransaction(
|
|
|
2493
2985
|
? [
|
|
2494
2986
|
input.actor.sessionId,
|
|
2495
2987
|
...cancellationSessionIds,
|
|
2496
|
-
...(
|
|
2497
|
-
? [cancellationSubtree.rootParentSessionId]
|
|
2498
|
-
: []),
|
|
2988
|
+
...(lineageParentSessionId ? [lineageParentSessionId] : []),
|
|
2499
2989
|
]
|
|
2500
|
-
: [
|
|
2501
|
-
...cancellationSessionIds,
|
|
2502
|
-
...(cancellationSubtree.rootParentSessionId
|
|
2503
|
-
? [cancellationSubtree.rootParentSessionId]
|
|
2504
|
-
: []),
|
|
2505
|
-
],
|
|
2990
|
+
: [...cancellationSessionIds, ...(lineageParentSessionId ? [lineageParentSessionId] : [])],
|
|
2506
2991
|
turnIds:
|
|
2507
2992
|
input.actor.type === "agent_attempt"
|
|
2508
2993
|
? [input.actor.turnId, ...cancellationTurnIds]
|
|
2509
2994
|
: cancellationTurnIds,
|
|
2510
2995
|
attemptIds: input.actor.type === "agent_attempt" ? [input.actor.attemptId] : [],
|
|
2511
2996
|
});
|
|
2512
|
-
if (input.action === "cancel") {
|
|
2997
|
+
if (input.action === "cancel" || pauseParentSessionId !== null) {
|
|
2513
2998
|
const rootSession = locks.sessions.find((session) => session.id === input.sessionId);
|
|
2514
|
-
if (!rootSession || rootSession.parentSessionId !==
|
|
2999
|
+
if (!rootSession || rootSession.parentSessionId !== lineageParentSessionId) {
|
|
2515
3000
|
throw new SessionControlInvariantError(
|
|
2516
|
-
`Session ${input.sessionId} parent changed while establishing
|
|
3001
|
+
`Session ${input.sessionId} parent changed while establishing ${input.action} locks`,
|
|
2517
3002
|
);
|
|
2518
3003
|
}
|
|
2519
3004
|
}
|
|
@@ -2543,6 +3028,7 @@ export async function mutateSessionControlInTransaction(
|
|
|
2543
3028
|
"Replayed session control receipt has no session event",
|
|
2544
3029
|
);
|
|
2545
3030
|
}
|
|
3031
|
+
const wakeCount = Number(reserved.receipt.result.wakeCount ?? 0);
|
|
2546
3032
|
return {
|
|
2547
3033
|
receipt: reserved.receipt,
|
|
2548
3034
|
control: await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
@@ -2551,7 +3037,8 @@ export async function mutateSessionControlInTransaction(
|
|
|
2551
3037
|
sessionControlEventId,
|
|
2552
3038
|
workspaceControlEventId,
|
|
2553
3039
|
interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
|
|
2554
|
-
|
|
3040
|
+
backgroundCommandCount: Number(reserved.receipt.result.backgroundCommandCount ?? 0),
|
|
3041
|
+
wakeCount,
|
|
2555
3042
|
cancelledSessionCount: Number(reserved.receipt.result.cancelledSessionCount ?? 0),
|
|
2556
3043
|
cancelledTurnCount: Number(reserved.receipt.result.cancelledTurnCount ?? 0),
|
|
2557
3044
|
affectedSessionEvents: Array.isArray(reserved.receipt.result.affectedSessionEvents)
|
|
@@ -2560,6 +3047,12 @@ export async function mutateSessionControlInTransaction(
|
|
|
2560
3047
|
eventIds: string[];
|
|
2561
3048
|
}>)
|
|
2562
3049
|
: [{ sessionId: input.sessionId, eventIds: [sessionControlEventId] }],
|
|
3050
|
+
workflowWake: await pendingSessionControlWorkflowWake(
|
|
3051
|
+
db,
|
|
3052
|
+
input.workspaceId,
|
|
3053
|
+
input.sessionId,
|
|
3054
|
+
wakeCount,
|
|
3055
|
+
),
|
|
2563
3056
|
replay: true,
|
|
2564
3057
|
};
|
|
2565
3058
|
}
|
|
@@ -2632,6 +3125,14 @@ export async function mutateSessionControlInTransaction(
|
|
|
2632
3125
|
input.actor.type === "agent_attempt"
|
|
2633
3126
|
? `attempt:${input.actor.attemptId}`
|
|
2634
3127
|
: input.actor.subjectId;
|
|
3128
|
+
const backgroundCommandCount =
|
|
3129
|
+
input.action === "pause" || input.action === "cancel"
|
|
3130
|
+
? await requestSessionBackgroundCommandCancellationsInTransaction(db, {
|
|
3131
|
+
workspaceId: input.workspaceId,
|
|
3132
|
+
rootSessionIds: [input.sessionId],
|
|
3133
|
+
subjectId: actor,
|
|
3134
|
+
})
|
|
3135
|
+
: 0;
|
|
2635
3136
|
const workspaceControlEventId = await insertWorkspaceControlEventInTransaction(db, {
|
|
2636
3137
|
accountId: input.accountId,
|
|
2637
3138
|
workspaceId: input.workspaceId,
|
|
@@ -2752,10 +3253,36 @@ export async function mutateSessionControlInTransaction(
|
|
|
2752
3253
|
cancelledTurnCount: 0,
|
|
2753
3254
|
affectedSessionEvents: [{ sessionId: input.sessionId, eventIds: [controlEvent.id] }],
|
|
2754
3255
|
};
|
|
3256
|
+
if (input.action === "pause" && pauseParentSessionId !== null) {
|
|
3257
|
+
// Direct Pause only (a recursive ancestor pause never reaches this command
|
|
3258
|
+
// for the child), and never when the parent's own live attempt issued it:
|
|
3259
|
+
// that parent already knows. Human/API pauses are action-required for the
|
|
3260
|
+
// parent; an agent pause is informational.
|
|
3261
|
+
const pausedSession = locks.sessions.find((session) => session.id === input.sessionId);
|
|
3262
|
+
const parentSession = locks.sessions.find((session) => session.id === pauseParentSessionId);
|
|
3263
|
+
const issuedByParentAttempt =
|
|
3264
|
+
input.actor.type === "agent_attempt" && input.actor.sessionId === pauseParentSessionId;
|
|
3265
|
+
if (pausedSession && parentSession && !issuedByParentAttempt) {
|
|
3266
|
+
await enqueueChildPausedOutboxInTransaction(db, {
|
|
3267
|
+
workspaceId: input.workspaceId,
|
|
3268
|
+
childSession: pausedSession,
|
|
3269
|
+
parentSession,
|
|
3270
|
+
operationId: reserved.receipt.id,
|
|
3271
|
+
actorKind:
|
|
3272
|
+
input.actor.type === "agent_attempt"
|
|
3273
|
+
? "agent"
|
|
3274
|
+
: input.actor.type === "service"
|
|
3275
|
+
? "api"
|
|
3276
|
+
: "human",
|
|
3277
|
+
reason: input.reason ?? null,
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
2755
3281
|
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
2756
3282
|
controlRevision: revision,
|
|
2757
3283
|
result: {
|
|
2758
3284
|
interruptionCount,
|
|
3285
|
+
backgroundCommandCount,
|
|
2759
3286
|
wakeCount,
|
|
2760
3287
|
eventId: controlEvent.id,
|
|
2761
3288
|
workspaceControlEventId,
|
|
@@ -2773,10 +3300,17 @@ export async function mutateSessionControlInTransaction(
|
|
|
2773
3300
|
sessionControlEventId: controlEvent.id,
|
|
2774
3301
|
workspaceControlEventId,
|
|
2775
3302
|
interruptionCount,
|
|
3303
|
+
backgroundCommandCount,
|
|
2776
3304
|
wakeCount,
|
|
2777
3305
|
cancelledSessionCount: cancellation.cancelledSessionCount,
|
|
2778
3306
|
cancelledTurnCount: cancellation.cancelledTurnCount,
|
|
2779
3307
|
affectedSessionEvents: cancellation.affectedSessionEvents,
|
|
3308
|
+
workflowWake: await pendingSessionControlWorkflowWake(
|
|
3309
|
+
db,
|
|
3310
|
+
input.workspaceId,
|
|
3311
|
+
input.sessionId,
|
|
3312
|
+
wakeCount,
|
|
3313
|
+
),
|
|
2780
3314
|
replay: false,
|
|
2781
3315
|
};
|
|
2782
3316
|
}
|
|
@@ -2787,8 +3321,15 @@ export async function autoResumeSessionBranchInTransaction(
|
|
|
2787
3321
|
workspaceId: string;
|
|
2788
3322
|
sessionId: string;
|
|
2789
3323
|
actor: string;
|
|
2790
|
-
reason: "
|
|
3324
|
+
reason: "human_steer" | "service_steer" | "agent_steer";
|
|
2791
3325
|
observedControlEtag?: string | null;
|
|
3326
|
+
/**
|
|
3327
|
+
* The admission prefix the caller already holds. A `share` admission
|
|
3328
|
+
* proves the branch was active under a lock that excludes every control
|
|
3329
|
+
* mutation, so it never needs to resume; only an `update` admission may
|
|
3330
|
+
* advance the workspace revision here.
|
|
3331
|
+
*/
|
|
3332
|
+
admission?: WorkspaceControlAdmissionLock;
|
|
2792
3333
|
},
|
|
2793
3334
|
): Promise<{
|
|
2794
3335
|
revision: number;
|
|
@@ -2796,10 +3337,12 @@ export async function autoResumeSessionBranchInTransaction(
|
|
|
2796
3337
|
changed: boolean;
|
|
2797
3338
|
workspaceControlEventId: string | null;
|
|
2798
3339
|
}> {
|
|
2799
|
-
const workspace =
|
|
3340
|
+
const workspace =
|
|
3341
|
+
input.admission?.control ??
|
|
3342
|
+
(await lockWorkspaceInferenceControl(db, input.workspaceId, "update"));
|
|
2800
3343
|
await assertSessionBranchIsNotCancelled(db, input.workspaceId, input.sessionId);
|
|
2801
3344
|
const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
2802
|
-
|
|
3345
|
+
workspaceControl: workspace,
|
|
2803
3346
|
});
|
|
2804
3347
|
if (input.observedControlEtag && input.observedControlEtag !== before.controlEtag) {
|
|
2805
3348
|
throw new SessionControlConflictError();
|
|
@@ -2812,6 +3355,13 @@ export async function autoResumeSessionBranchInTransaction(
|
|
|
2812
3355
|
workspaceControlEventId: null,
|
|
2813
3356
|
};
|
|
2814
3357
|
}
|
|
3358
|
+
if (input.admission?.mode === "share") {
|
|
3359
|
+
// Impossible by construction: the shared prefix observed an active branch
|
|
3360
|
+
// and excludes every control mutation until this transaction ends.
|
|
3361
|
+
throw new SessionControlInvariantError(
|
|
3362
|
+
`Session ${input.sessionId} became paused under a shared workspace control lock`,
|
|
3363
|
+
);
|
|
3364
|
+
}
|
|
2815
3365
|
const revision = nextRevision(workspace);
|
|
2816
3366
|
await advanceWorkspaceRevision(db, input.workspaceId, revision);
|
|
2817
3367
|
await db
|
|
@@ -2859,6 +3409,7 @@ export type WorkspaceControlMutationResult = {
|
|
|
2859
3409
|
workspaceControlEventId: string;
|
|
2860
3410
|
workspaceState: EffectiveControlState;
|
|
2861
3411
|
interruptionCount: number;
|
|
3412
|
+
backgroundCommandCount: number;
|
|
2862
3413
|
wakeCount: number;
|
|
2863
3414
|
replay: boolean;
|
|
2864
3415
|
};
|
|
@@ -2873,9 +3424,15 @@ export async function mutateWorkspaceControlInTransaction(
|
|
|
2873
3424
|
action: "pause" | "resume";
|
|
2874
3425
|
reason?: string | null;
|
|
2875
3426
|
expectedRevision?: number | null;
|
|
3427
|
+
/** Request-scoped callers bound the control prefix wait; lifecycle callers omit it. */
|
|
3428
|
+
controlLockTimeoutMs?: number;
|
|
2876
3429
|
},
|
|
2877
3430
|
): Promise<WorkspaceControlMutationResult> {
|
|
2878
|
-
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update"
|
|
3431
|
+
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update", {
|
|
3432
|
+
...(input.controlLockTimeoutMs !== undefined
|
|
3433
|
+
? { lockTimeoutMs: input.controlLockTimeoutMs }
|
|
3434
|
+
: {}),
|
|
3435
|
+
});
|
|
2879
3436
|
const currentRevision = asSafeRevision(workspace.revision, "workspace control revision")!;
|
|
2880
3437
|
const hash = canonicalSessionCommandHash({
|
|
2881
3438
|
action: input.action,
|
|
@@ -2903,6 +3460,7 @@ export async function mutateWorkspaceControlInTransaction(
|
|
|
2903
3460
|
workspaceControlEventId,
|
|
2904
3461
|
workspaceState: input.action === "pause" ? "paused" : "active",
|
|
2905
3462
|
interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
|
|
3463
|
+
backgroundCommandCount: Number(reserved.receipt.result.backgroundCommandCount ?? 0),
|
|
2906
3464
|
wakeCount: Number(reserved.receipt.result.wakeCount ?? 0),
|
|
2907
3465
|
replay: true,
|
|
2908
3466
|
};
|
|
@@ -2916,6 +3474,13 @@ export async function mutateWorkspaceControlInTransaction(
|
|
|
2916
3474
|
input.actor.type === "agent_attempt"
|
|
2917
3475
|
? `attempt:${input.actor.attemptId}`
|
|
2918
3476
|
: input.actor.subjectId;
|
|
3477
|
+
const backgroundCommandCount =
|
|
3478
|
+
input.action === "pause"
|
|
3479
|
+
? await requestWorkspaceBackgroundCommandCancellationsInTransaction(db, {
|
|
3480
|
+
workspaceId: input.workspaceId,
|
|
3481
|
+
subjectId: actor,
|
|
3482
|
+
})
|
|
3483
|
+
: 0;
|
|
2919
3484
|
const [updated] = await db
|
|
2920
3485
|
.update(schema.workspaceInferenceControls)
|
|
2921
3486
|
.set({
|
|
@@ -2968,7 +3533,7 @@ export async function mutateWorkspaceControlInTransaction(
|
|
|
2968
3533
|
});
|
|
2969
3534
|
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
2970
3535
|
controlRevision: revision,
|
|
2971
|
-
result: { interruptionCount, wakeCount, workspaceControlEventId },
|
|
3536
|
+
result: { interruptionCount, backgroundCommandCount, wakeCount, workspaceControlEventId },
|
|
2972
3537
|
});
|
|
2973
3538
|
return {
|
|
2974
3539
|
receipt,
|
|
@@ -2976,6 +3541,7 @@ export async function mutateWorkspaceControlInTransaction(
|
|
|
2976
3541
|
workspaceControlEventId,
|
|
2977
3542
|
workspaceState: input.action === "pause" ? "paused" : "active",
|
|
2978
3543
|
interruptionCount,
|
|
3544
|
+
backgroundCommandCount,
|
|
2979
3545
|
wakeCount,
|
|
2980
3546
|
replay: false,
|
|
2981
3547
|
};
|