From 9463cf41c3b236ef5133f40e513fa74eecd60c2e Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:20:28 -0700 Subject: [PATCH] Extract automation API operations into runtime layer (#37196) * Extract automation API operations into a separate runtime layer * Fix Source Control test fixtures for remote operation state --- .../automation-api-operations.ts | 134 ++++++++++++++++++ .../automation-operations.ts | 134 +----------------- 2 files changed, 136 insertions(+), 132 deletions(-) create mode 100644 src/main/runtime/workspaces/persisted-surfaces/automation-api-operations.ts diff --git a/src/main/runtime/workspaces/persisted-surfaces/automation-api-operations.ts b/src/main/runtime/workspaces/persisted-surfaces/automation-api-operations.ts new file mode 100644 index 00000000000..1975c24bbfe --- /dev/null +++ b/src/main/runtime/workspaces/persisted-surfaces/automation-api-operations.ts @@ -0,0 +1,134 @@ +import type { AutomationsChangedPayload } from '../../../../shared/runtime-client-events' +import type { + AutomationOwnerPrecondition, + AutomationDestination, + AutomationUpdatePreconditions +} from '../../../../shared/automation-owner-precondition' +import type { Automation, AutomationRun } from '../../../../shared/automations-types' +import type { + RuntimeAutomationCreateInput, + RuntimeAutomationUpdateInput +} from '../../runtime-automation-controller' +import { + automationChangePublications, + type AutomationChangeSelector, + type AutomationListResult +} from '../../../../shared/automation-list-scope' +import { OrcaRuntimeWithCrewOperations } from './crew-operations' + +export abstract class OrcaRuntimeWithAutomationApiOperations extends OrcaRuntimeWithCrewOperations { + abstract notifyAutomationsChanged(payload?: AutomationsChangedPayload): void + + private readAutomationRuns( + automationId: string | undefined, + expectedOwner: AutomationOwnerPrecondition | undefined, + read: () => T + ): T { + if (expectedOwner && !automationId) { + throw new Error('An expected owner requires an automation id.') + } + return this.automation.withExternalProbePriority(() => { + if (automationId) { + this.automation.assertOwner(automationId, expectedOwner, 'read') + } + return read() + }) + } + + listAutomationRuns( + automationId?: string, + expectedOwner?: AutomationOwnerPrecondition + ): AutomationRun[] { + return this.readAutomationRuns(automationId, expectedOwner, () => + this.automation.listRuns(automationId) + ) + } + + listAutomationRunsPage( + automationId?: string, + expectedOwner?: AutomationOwnerPrecondition, + limit?: number, + cursor?: string + ) { + return this.readAutomationRuns(automationId, expectedOwner, () => + this.automation.listRunsPage(automationId, limit, cursor) + ) + } + + showAutomation(id: string, expectedOwner?: AutomationOwnerPrecondition): Automation { + const automation = this.automation.show(id) + this.automation.assertOwner(id, expectedOwner, 'read') + return automation + } + + protected automationChangeSelector(id: string): AutomationChangeSelector | null { + return this.store?.automationChangeSelector?.(id) ?? null + } + + /** A store that cannot name the affected host degrades to one unscoped authority event. */ + protected publishAutomationDefinitionChange( + before: AutomationChangeSelector | null, + after: AutomationChangeSelector | null + ): void { + for (const selector of automationChangePublications(before, after)) { + this.notifyAutomationsChanged({ reason: 'definition', ...(selector ? { selector } : {}) }) + } + } + + createAutomation( + input: RuntimeAutomationCreateInput, + destination?: AutomationDestination + ): Promise { + return this.automation.withExternalProbePriority(() => + this.automation.create(input, destination).then((automation) => { + const selector = this.automationChangeSelector(automation.id) + this.publishAutomationDefinitionChange(selector, selector) + return automation + }) + ) + } + + updateAutomation( + id: string, + updates: RuntimeAutomationUpdateInput, + options?: AutomationUpdatePreconditions + ): Promise { + return this.automation.withExternalProbePriority(() => { + const source = this.automationChangeSelector(id) + return this.automation.update(id, updates, options).then((automation) => { + this.publishAutomationDefinitionChange(source, this.automationChangeSelector(automation.id)) + return automation + }) + }) + } + + deleteAutomation( + id: string, + expectedOwner?: AutomationOwnerPrecondition + ): Promise<{ removed: boolean; id: string }> { + return this.automation.withExternalProbePriority(() => { + const selector = this.automationChangeSelector(id) + return this.automation.delete(id, expectedOwner).then((result) => { + this.publishAutomationDefinitionChange(selector, selector) + return result + }) + }) + } + + runAutomationNow( + id: string, + expectedOwner?: AutomationOwnerPrecondition + ): Promise { + return this.automation.withExternalProbePriority(() => + this.automation.runNow(id, expectedOwner) + ) + } + + listAutomationsForScope(params = {}): AutomationListResult { + return this.automation.withExternalProbePriority(() => this.automation.listForScope(params)) + } + + automationOwnerPrecondition(id: string): AutomationOwnerPrecondition | null { + return this.automation.ownerPrecondition(id) + } +} diff --git a/src/main/runtime/workspaces/persisted-surfaces/automation-operations.ts b/src/main/runtime/workspaces/persisted-surfaces/automation-operations.ts index f5dbd6963c0..64a37654e2d 100644 --- a/src/main/runtime/workspaces/persisted-surfaces/automation-operations.ts +++ b/src/main/runtime/workspaces/persisted-surfaces/automation-operations.ts @@ -1,22 +1,6 @@ -import type { AutomationsChangedPayload } from '../../../../shared/runtime-client-events' import { getWorkerTerminalAutoReleaseScheduler } from '../../orchestration/worker-terminal-auto-release-scheduler' import type { ResolvedWorktree } from '../../runtime-worktree-path-identity' -import { OrcaRuntimeWithCrewOperations } from './crew-operations' -import type { - AutomationOwnerPrecondition, - AutomationDestination, - AutomationUpdatePreconditions -} from '../../../../shared/automation-owner-precondition' -import type { Automation, AutomationRun } from '../../../../shared/automations-types' -import type { - RuntimeAutomationCreateInput, - RuntimeAutomationUpdateInput -} from '../../runtime-automation-controller' -import { - automationChangePublications, - type AutomationChangeSelector, - type AutomationListResult -} from '../../../../shared/automation-list-scope' +import { OrcaRuntimeWithAutomationApiOperations } from './automation-api-operations' import { OrchestrationDb } from '../../orchestration/db' import { committedHandBackTypes, @@ -26,8 +10,7 @@ import { existsSync } from 'node:fs' import type { LegacyWorkerTerminalRecoveryPlan } from '../../orchestration/orchestration-legacy-worker-terminal-recovery' import type { LegacyWorkerTerminalRecoveryResult } from '../../runtime-legacy-worker-terminal-recovery-types' -export abstract class OrcaRuntimeWithAutomationOperations extends OrcaRuntimeWithCrewOperations { - abstract notifyAutomationsChanged(payload?: AutomationsChangedPayload): void +export abstract class OrcaRuntimeWithAutomationOperations extends OrcaRuntimeWithAutomationApiOperations { abstract ensureOrchestrationFederationRelay(runId?: string): void protected abstract scheduleRestoredMessageRepoints(): void /** Mail a worker left unread when it reported before a restart still continues it. */ @@ -35,119 +18,6 @@ export abstract class OrcaRuntimeWithAutomationOperations extends OrcaRuntimeWit protected abstract getResolvedWorktreeMap(): Promise> protected abstract getLeafKey(tabId: string, leafId: string): string - private readAutomationRuns( - automationId: string | undefined, - expectedOwner: AutomationOwnerPrecondition | undefined, - read: () => T - ): T { - if (expectedOwner && !automationId) { - throw new Error('An expected owner requires an automation id.') - } - return this.automation.withExternalProbePriority(() => { - if (automationId) { - this.automation.assertOwner(automationId, expectedOwner, 'read') - } - return read() - }) - } - - listAutomationRuns( - automationId?: string, - expectedOwner?: AutomationOwnerPrecondition - ): AutomationRun[] { - return this.readAutomationRuns(automationId, expectedOwner, () => - this.automation.listRuns(automationId) - ) - } - - listAutomationRunsPage( - automationId?: string, - expectedOwner?: AutomationOwnerPrecondition, - limit?: number, - cursor?: string - ) { - return this.readAutomationRuns(automationId, expectedOwner, () => - this.automation.listRunsPage(automationId, limit, cursor) - ) - } - - showAutomation(id: string, expectedOwner?: AutomationOwnerPrecondition): Automation { - const automation = this.automation.show(id) - this.automation.assertOwner(id, expectedOwner, 'read') - return automation - } - - protected automationChangeSelector(id: string): AutomationChangeSelector | null { - return this.store?.automationChangeSelector?.(id) ?? null - } - - /** A store that cannot name the affected host degrades to one unscoped authority event. */ - protected publishAutomationDefinitionChange( - before: AutomationChangeSelector | null, - after: AutomationChangeSelector | null - ): void { - for (const selector of automationChangePublications(before, after)) { - this.notifyAutomationsChanged({ reason: 'definition', ...(selector ? { selector } : {}) }) - } - } - - createAutomation( - input: RuntimeAutomationCreateInput, - destination?: AutomationDestination - ): Promise { - return this.automation.withExternalProbePriority(() => - this.automation.create(input, destination).then((automation) => { - const selector = this.automationChangeSelector(automation.id) - this.publishAutomationDefinitionChange(selector, selector) - return automation - }) - ) - } - - updateAutomation( - id: string, - updates: RuntimeAutomationUpdateInput, - options?: AutomationUpdatePreconditions - ): Promise { - return this.automation.withExternalProbePriority(() => { - const source = this.automationChangeSelector(id) - return this.automation.update(id, updates, options).then((automation) => { - this.publishAutomationDefinitionChange(source, this.automationChangeSelector(automation.id)) - return automation - }) - }) - } - - deleteAutomation( - id: string, - expectedOwner?: AutomationOwnerPrecondition - ): Promise<{ removed: boolean; id: string }> { - return this.automation.withExternalProbePriority(() => { - const selector = this.automationChangeSelector(id) - return this.automation.delete(id, expectedOwner).then((result) => { - this.publishAutomationDefinitionChange(selector, selector) - return result - }) - }) - } - - runAutomationNow( - id: string, - expectedOwner?: AutomationOwnerPrecondition - ): Promise { - return this.automation.withExternalProbePriority(() => - this.automation.runNow(id, expectedOwner) - ) - } - - listAutomationsForScope(params = {}): AutomationListResult { - return this.automation.withExternalProbePriority(() => this.automation.listForScope(params)) - } - - automationOwnerPrecondition(id: string): AutomationOwnerPrecondition | null { - return this.automation.ownerPrecondition(id) - } - // Defer until userData is final; tests can inject an in-memory DB. getOrchestrationDb(): OrchestrationDb { if (!this._orchestrationDb) {