From 9e180a774cc5a42d1b5b8b7167e3584dfb100818 Mon Sep 17 00:00:00 2001 From: Wang Date: Fri, 4 Sep 2026 13:30:08 +0800 Subject: [PATCH] fix(cli): repair legacy Host controls before update Regenerate legacy operator and reconciliation artifacts before strict projection verification, without touching the running Host supervisor. This lets Desktop recovery safely replace managed Hosts created before the operator.mjs migration.\n\nGenerated-by: Codex --- ...runtime-host-lifecycle-transaction.test.ts | 37 +++++++++++++++++++ .../runtime-host-selected-update.test.ts | 10 ++++- .../src/runtime-host-lifecycle-transaction.ts | 19 ++++++++++ .../cli/src/runtime-host-update-command.ts | 5 ++- 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/__tests__/runtime-host-lifecycle-transaction.test.ts b/packages/cli/src/__tests__/runtime-host-lifecycle-transaction.test.ts index be78154c77..2030626f6c 100644 --- a/packages/cli/src/__tests__/runtime-host-lifecycle-transaction.test.ts +++ b/packages/cli/src/__tests__/runtime-host-lifecycle-transaction.test.ts @@ -43,6 +43,7 @@ import type { import { assertRuntimeHostManagedOperatorConfig } from '../runtime-host-managed-deployment.js'; import { applyRuntimeHostLifecycleTransition, + convergeRuntimeHostLifecycleControlProjection, recoverRuntimeHostLifecycleTransition, replaceRuntimeHostLifecycle, resolveRecoverableRuntimeHostManagedDeployment, @@ -56,6 +57,42 @@ import { const INTEGRITY = `sha512-${Buffer.alloc(64, 7).toString('base64')}`; const UPDATED_INTEGRITY = `sha512-${Buffer.alloc(64, 8).toString('base64')}`; +test('control projection repair leaves the running Host supervisor untouched', async () => { + const current = config('/workspace', 'a'.repeat(64), 1, 'launch_agent'); + const calls: string[] = []; + const provider = new FakeLifecycleProvider('launch_agent', 'launch_agent_timer'); + const originalActivate = provider.reconciliationTrigger.activate; + provider.reconciliationTrigger.activate = async () => { + calls.push('scheduler.activate'); + await originalActivate(); + }; + + await convergeRuntimeHostLifecycleControlProjection(current, { + convergeOperator: async (from, to) => { + assert.deepEqual(from, current); + assert.deepEqual(to, current); + calls.push('operator.converge'); + }, + verifyOperator: async () => undefined, + resolveProvider: () => ({ + ...provider, + supervisor: { + ...provider.supervisor, + converge: async () => assert.fail('the running supervisor must not be replaced'), + }, + reconciliationTrigger: { + ...provider.reconciliationTrigger, + converge: async (definition) => { + calls.push('scheduler.converge'); + await provider.reconciliationTrigger.converge(definition); + }, + }, + }), + }); + + assert.deepEqual(calls, ['operator.converge', 'scheduler.converge', 'scheduler.activate']); +}); + test('one authority record recovers provider cutover failures without a journal', async (t) => { const stateRoot = await mkdtemp(join(tmpdir(), 'maka-lifecycle-root-')); const authorityRoot = await mkdtemp(join(tmpdir(), 'maka-lifecycle-authority-')); diff --git a/packages/cli/src/__tests__/runtime-host-selected-update.test.ts b/packages/cli/src/__tests__/runtime-host-selected-update.test.ts index 09564bc2ec..dfc025ab9d 100644 --- a/packages/cli/src/__tests__/runtime-host-selected-update.test.ts +++ b/packages/cli/src/__tests__/runtime-host-selected-update.test.ts @@ -84,6 +84,7 @@ describe('managed Runtime Host selected update', () => { let output = ''; let managedReads = 0; let pruned = false; + const projection: string[] = []; const exitCode = await runManagedRuntimeHostUpdateCli( { ...OPTIONS, @@ -106,7 +107,13 @@ describe('managed Runtime Host selected update', () => { assert.deepEqual(options?.expectedOwner, expectedHost); return { kind: 'active', config: current } as never; }, - verifyProjection: async () => {}, + convergeControlProjection: async (config) => { + assert.equal(config, current); + projection.push('converge'); + }, + verifyProjection: async () => { + projection.push('verify'); + }, assertOperatorConfig: () => {}, manageLifecycle: async () => { managedReads += 1; @@ -130,6 +137,7 @@ describe('managed Runtime Host selected update', () => { assert.equal(exitCode, 0); assert.equal(managedReads, 2); assert.equal(pruned, true); + assert.deepEqual(projection, ['converge', 'verify']); assert.doesNotMatch(output, /"phase":"staging"/u); const result = decodeRuntimeHostServiceManagementFrame(output.trim().split('\n').at(-1) ?? ''); assert.equal( diff --git a/packages/cli/src/runtime-host-lifecycle-transaction.ts b/packages/cli/src/runtime-host-lifecycle-transaction.ts index 48b3247cdf..8015c39235 100644 --- a/packages/cli/src/runtime-host-lifecycle-transaction.ts +++ b/packages/cli/src/runtime-host-lifecycle-transaction.ts @@ -806,6 +806,25 @@ export async function verifyRuntimeHostLifecycleReady( ); } +/** Repairs update-control artifacts without interrupting the running Host supervisor. */ +export async function convergeRuntimeHostLifecycleControlProjection( + config: RuntimeHostManagedDeploymentConfig, + deps: RuntimeHostLifecycleTransactionDeps, +): Promise { + const canonical = decodeRuntimeHostManagedDeploymentConfig(config); + await deps.convergeOperator(canonical, canonical); + if (canonical.lifecycle.mode !== 'supervised') return; + const provider = deps.resolveProvider(canonical); + if (canonical.reconciliation.trigger === 'scheduled') { + await provider.reconciliationTrigger.converge( + runtimeHostReconciliationTriggerDefinition(canonical), + ); + await provider.reconciliationTrigger.activate(); + } else { + await provider.reconciliationTrigger.uninstall(); + } +} + /** Verifies the durable operator and supervisor projection without requiring a compatible Host. */ export async function verifyRuntimeHostLifecycleProjection( config: RuntimeHostManagedDeploymentConfig, diff --git a/packages/cli/src/runtime-host-update-command.ts b/packages/cli/src/runtime-host-update-command.ts index dc8d755767..6c20ac2f66 100644 --- a/packages/cli/src/runtime-host-update-command.ts +++ b/packages/cli/src/runtime-host-update-command.ts @@ -23,7 +23,6 @@ import { truncateUtf8 } from '@maka/core/diagnostic-log'; import { activateRuntimeHostManagedDeployment } from '@maka/runtime-host/client'; import { createRuntimeHostLegacyPosixOperatorCommand, - runtimeHostManagedOperatorCommand, decodeRuntimeHostServiceManagementFrame, encodeRuntimeHostServiceManagementFrame, RUNTIME_HOST_SERVICE_ERROR_CODE_MAX_BYTES, @@ -81,6 +80,7 @@ import { import type { RuntimeHostExpectedHost, RuntimeHostUpdateSelector } from './runtime-host-cli.js'; import { canDiscardRuntimeHostLifecycleDesiredArtifacts, + convergeRuntimeHostLifecycleControlProjection, replaceRuntimeHostLifecycle, resolveRecoverableRuntimeHostManagedDeployment, RuntimeHostLifecycleTransactionError, @@ -142,6 +142,7 @@ interface RuntimeHostUpdateCliDeps { readonly createLifecycleDeps: (rootId: string) => RuntimeHostLifecycleTransactionDeps; readonly assertOperatorDeployment: typeof assertRuntimeHostManagedOperatorDeployment; readonly recoverDeployment: typeof resolveRecoverableRuntimeHostManagedDeployment; + readonly convergeControlProjection: typeof convergeRuntimeHostLifecycleControlProjection; readonly verifyProjection: typeof verifyRuntimeHostLifecycleProjection; readonly assertOperatorConfig: typeof assertRuntimeHostManagedOperatorConfig; readonly manageLifecycle: typeof manageRuntimeHostManagedLifecycle; @@ -237,6 +238,7 @@ export async function runManagedRuntimeHostUpdateCli( }), assertOperatorDeployment: assertRuntimeHostManagedOperatorDeployment, recoverDeployment: resolveRecoverableRuntimeHostManagedDeployment, + convergeControlProjection: convergeRuntimeHostLifecycleControlProjection, verifyProjection: verifyRuntimeHostLifecycleProjection, assertOperatorConfig: assertRuntimeHostManagedOperatorConfig, manageLifecycle: manageRuntimeHostManagedLifecycle, @@ -651,6 +653,7 @@ async function runCanonicalRuntimeHostUpdate( ); } const current = recovered.config; + await deps.canonical.convergeControlProjection(current, lifecycleDeps); await deps.canonical.verifyProjection(current, lifecycleDeps); deps.canonical.assertOperatorConfig( current,