mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* fix(ssh): move enabled SSH automations to the host's Orca server
Connecting an SSH host moves its saved state to the host's managed Orca
server. An enabled SSH automation refused that move on every Connect
("automations (1), automation runs (4)"), leaving the host unconnectable;
Move anyway connected it but stranded the automation on the desktop, where
it no longer ran.
An enabled automation whose runs have all finished now moves with its run
history. The manifest still carries it disabled, which every server version
accepts; once the move commits the desktop disables its retained copy and
turns the server's copy back on through automation.update, re-sending its
start time so the next run is the next future occurrence. A server that
can't is noted in Settings; an unreachable one is retried on the next
Connect. While a move fences the host, the desktop scheduler, Run now and
the launch re-check write nothing for its automations, so a tick can no
longer break the fenced export; releasing the fence resumes them. A run
still in flight keeps blocking and the refusal now says to wait for it.
Fixes #29083
* test(ssh): mock the server automation enabler in the delta-move handler test
* test(e2e): keep the Move anyway specs refusing now that enabled automations move
The inline Move anyway spec seeded an enabled automation to force a
refusal, which this branch now moves instead. It seeds one pinned to an
earlier registration of the host, which still stays behind. The relay
bridge spec's Move anyway lookup also matched the refusal toast's
button since #34041; it now takes the pane's own.
224 lines
8.0 KiB
TypeScript
224 lines
8.0 KiB
TypeScript
import type { PublicKnownRuntimeEnvironment } from './runtime-environments'
|
|
import type { OrcadTerminalCensus } from './orcad-terminal-census'
|
|
import type { OrcadMigrationBlocker } from './orcad-migration/orcad-migration-preflight'
|
|
import type { OrcadMigrationSourceCutoverPhase } from './orcad-migration/orcad-migration-source-cutover'
|
|
import type { OrcadDaemonRetirementVerdict } from './orcad-stop-request'
|
|
import type { SshManagedServerLinkedProject } from './ssh-types'
|
|
import type { OrcadStaleLockEvidence } from './orcad-stale-lock-reclaim'
|
|
|
|
export const ORCAD_MANAGED_REMOTE_PORT = 6_768
|
|
|
|
/** Refused because terminals are, or may be, still running on the host. */
|
|
export type OrcadManagedRefusal = {
|
|
outcome: 'refused'
|
|
verdict: 'live' | 'unverifiable'
|
|
code: string
|
|
reason: string
|
|
/** Only saved state blocks, which a confirmed "Move anyway" leaves on this desktop. */
|
|
canLeaveBehind?: true
|
|
}
|
|
|
|
export type OrcadManagedDeferral = {
|
|
outcome: 'deferred'
|
|
candidateVersion: string
|
|
code: string
|
|
reason: string
|
|
/** False when force cannot make the rejected lifecycle transition safe. */
|
|
forceable?: boolean
|
|
}
|
|
|
|
/** The one deferral Update anyway may pass: counted terminals the daemon carries across. */
|
|
export const ORCAD_UPDATE_TERMINALS_RUNNING_CODE = 'orcad_update_terminals_running'
|
|
|
|
/** Update anyway passes counted live-terminal deferrals, preserves version admission, and requires the confirmed incumbent build. */
|
|
export type OrcadManagedPastLiveTerminals = { incumbentVersion: string | null }
|
|
|
|
export type OrcadManagedDeployResult =
|
|
| {
|
|
outcome: 'created' | 'updated' | 'already-current'
|
|
environment: PublicKnownRuntimeEnvironment
|
|
activeVersion: string
|
|
}
|
|
| OrcadManagedDeferral
|
|
|
|
export type OrcadManagedRollbackResult =
|
|
| {
|
|
outcome: 'rolled-back'
|
|
environment: PublicKnownRuntimeEnvironment
|
|
activeVersion: string
|
|
discarded: string[]
|
|
}
|
|
| { outcome: 'refused' | 'failed'; code: string; reason: string }
|
|
|
|
/** A recovery that needs the operator to accept restoring a snapshot over changed state. */
|
|
export const ORCAD_RECOVERY_CHANGED_STATE_CODE = 'orcad_recovery_changed_state'
|
|
|
|
export type OrcadManagedRecoveryResult =
|
|
| { outcome: 'none' }
|
|
| { outcome: 'pending'; code: string; reason: string }
|
|
| {
|
|
outcome: 'recovered'
|
|
resolution: 'committed' | 'restored-incumbent'
|
|
activeVersion: string | null
|
|
environment: PublicKnownRuntimeEnvironment
|
|
}
|
|
| OrcadManagedRefusal
|
|
|
|
/** Only a proven `exited` unlinks the server locally; anything less keeps it linked. */
|
|
export type OrcadManagedStopResult =
|
|
| {
|
|
outcome: 'unlinked'
|
|
verdict: 'exited'
|
|
environmentId: string
|
|
sshTargetId: string
|
|
stoppedVersion: string | null
|
|
retirement: OrcadDaemonRetirementVerdict | null
|
|
}
|
|
| OrcadManagedRefusal
|
|
|
|
/** Refuse restart with live or unverifiable terminals: the daemon outlives orcad and retains its inherited environment. */
|
|
export type OrcadManagedRestartResult =
|
|
| { outcome: 'restarted'; environmentId: string }
|
|
| { outcome: 'terminals-running'; liveSessions: number | null }
|
|
| { outcome: 'not-restarted'; reason: string }
|
|
|
|
/** A dry run, or a confirmation pinned to the lock record that dry run showed. */
|
|
export type OrcadManagedStaleLockReclaimRequest =
|
|
| { yes: false }
|
|
| { yes: true; expectNonce: string }
|
|
|
|
/** `orcad --reclaim-stale-lock` run on a managed host's active slot (#32177). */
|
|
export type OrcadManagedStaleLockReclaimResult =
|
|
| {
|
|
status: 'none' | 'eligible' | 'refused'
|
|
message: string
|
|
lockPath: string
|
|
evidence?: OrcadStaleLockEvidence
|
|
}
|
|
/** `notRestarted` says why the server did not start again once its lock was cleared. */
|
|
| {
|
|
status: 'reclaimed'
|
|
environmentId: string
|
|
message: string
|
|
lockPath: string
|
|
/** The record the reclaim acted on. */
|
|
evidence?: OrcadStaleLockEvidence
|
|
notRestarted?: string
|
|
}
|
|
/** No remote reclaim occurred. reclaimWithoutPin requires manual reclaim because the host cannot pin confirmation;
|
|
* otherwise lockPath names the lock to delete manually. */
|
|
| { status: 'unsupported'; message: string; lockPath?: string; reclaimWithoutPin?: true }
|
|
/** The slot runs the command but gave no answer this client understands; nothing is known. */
|
|
| { status: 'no-answer'; message: string }
|
|
/** Another lifecycle step holds the host; nothing changed. */
|
|
| { status: 'busy'; message: string }
|
|
|
|
/** Forget only unlinks this desktop; an unreachable server may still run, so its verdict remains unverifiable. */
|
|
export type OrcadManagedForgetResult =
|
|
| {
|
|
outcome: 'forgotten'
|
|
verdict: 'unverifiable'
|
|
environmentId: string
|
|
sshTargetId: string
|
|
}
|
|
| OrcadManagedRefusal
|
|
|
|
export type OrcadManagedCancelStopResult =
|
|
| { outcome: 'none' }
|
|
/** The stop was withdrawn before orcad acted on it; the server keeps serving. */
|
|
| { outcome: 'canceled'; activeVersion: string }
|
|
/** orcad had already exited; finish with stop to unlink the server. */
|
|
| { outcome: 'already-stopped' }
|
|
| OrcadManagedRefusal
|
|
|
|
export type OrcadManagedRuntimeStatus = {
|
|
environmentId: string
|
|
sshTargetId: string
|
|
activeVersion: string | null
|
|
previousVersion: string | null
|
|
activatedAt: string | null
|
|
rollbackAvailable: boolean
|
|
recovery:
|
|
| {
|
|
operation: 'activate'
|
|
phase: 'prepared' | 'incumbent-stopped' | 'snapshot-captured' | 'candidate-ready'
|
|
version: string
|
|
startedAt: string
|
|
}
|
|
| {
|
|
operation: 'rollback'
|
|
phase:
|
|
| 'prepared'
|
|
| 'incumbent-stopped'
|
|
| 'rescue-captured'
|
|
| 'rollback-state-restored'
|
|
| 'target-ready'
|
|
version: string
|
|
startedAt: string
|
|
}
|
|
| {
|
|
operation: 'decommission'
|
|
phase: 'prepared' | 'stop-dispatched' | 'process-exited'
|
|
version: string
|
|
startedAt: string
|
|
}
|
|
| null
|
|
/** Terminals the daemon runs; `null` counts are unverifiable and block updates and stops. */
|
|
terminals: OrcadTerminalCensus
|
|
/** An unfinished dormant migration into this server; a rollback is refused while it runs. */
|
|
migration: {
|
|
migrationId: string
|
|
phase: OrcadMigrationSourceCutoverPhase
|
|
startedAt: string
|
|
} | null
|
|
/** The last update this client deferred for this server, cleared once one goes through. */
|
|
deferredUpdate: (OrcadManagedDeferral & { deferredAt: string }) | null
|
|
/** The Orca release that activated `activeVersion`; absent when unknown or from older desktops. */
|
|
activeAppVersion?: string | null
|
|
/** This desktop's bundled build when Update would move the host to it; absent from older desktops. */
|
|
availableUpdate?: string | null
|
|
}
|
|
|
|
export type OrcadManagedConversionResult =
|
|
| {
|
|
/** Committed on the server and retired from the SSH host. */
|
|
outcome: 'converted'
|
|
environment: PublicKnownRuntimeEnvironment
|
|
migrationId: string
|
|
/** Projects the server already held, so this desktop's own state for them did not move. */
|
|
linkedProjects?: SshManagedServerLinkedProject[]
|
|
/** Moved automations the server would not re-enable; they stay off there. */
|
|
automationsLeftDisabled?: number
|
|
}
|
|
| OrcadManagedDeferral
|
|
| OrcadManagedRefusal
|
|
|
|
export type OrcadManagedPendingMigrationRow = {
|
|
migrationId: string
|
|
environmentId: string
|
|
name: string
|
|
sshTargetId: string
|
|
phase: OrcadMigrationSourceCutoverPhase
|
|
startedAt: string
|
|
}
|
|
|
|
export type OrcadDeltaMoveRow = {
|
|
kind: 'repository' | 'folder-workspace' | 'project-group'
|
|
id: string
|
|
label: string
|
|
}
|
|
|
|
/** What moving a host's newer projects would add, and what the server will keep as it is. */
|
|
export type OrcadDeltaMovePreview = {
|
|
sshTargetId: string
|
|
environmentId: string
|
|
added: OrcadDeltaMoveRow[]
|
|
/** Projects an older build changed or removed; the server keeps its own copy of these. */
|
|
notReflected: { edited: OrcadDeltaMoveRow[]; removed: OrcadDeltaMoveRow[] }
|
|
blockers: OrcadMigrationBlocker[]
|
|
}
|
|
|
|
export type OrcadDeltaMoveResult =
|
|
| { outcome: 'moved'; migrationId: string }
|
|
| { outcome: 'refused'; code: string; reason: string; blockers?: OrcadMigrationBlocker[] }
|